From bf244e2bfaf8173b5552d36e7721d6e99040d4c5 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Wed, 16 Dec 2015 19:03:01 +0100 Subject: [PATCH 01/94] Add padding-bottom to ascribe-property instead of ascribe-property-wrapper Avoids having to style all the collapsible Properties with `padding-bottom: 0` to center labels when collapsed. --- js/components/ascribe_forms/form_consign.js | 3 +-- js/components/ascribe_forms/form_copyright_association.js | 5 ++--- js/components/ascribe_forms/form_loan.js | 3 +-- .../ascribe_forms/form_send_contract_agreement.js | 3 +-- js/components/ascribe_forms/form_signup.js | 5 ++--- js/components/ascribe_forms/form_submit_to_prize.js | 3 +-- .../ascribe_forms/input_contract_agreement_checkbox.js | 2 +- js/components/ascribe_settings/account_settings.js | 5 ++--- .../components/pr_forms/pr_register_piece_form.js | 3 +-- .../prize/simple_prize/components/prize_register_piece.js | 3 +-- sass/ascribe_property.scss | 8 ++------ 11 files changed, 15 insertions(+), 28 deletions(-) diff --git a/js/components/ascribe_forms/form_consign.js b/js/components/ascribe_forms/form_consign.js index 2f0ebf05..c92f4b6f 100644 --- a/js/components/ascribe_forms/form_consign.js +++ b/js/components/ascribe_forms/form_consign.js @@ -115,8 +115,7 @@ let ConsignForm = React.createClass({ + className="ascribe-property-collapsible-toggle"> diff --git a/js/components/ascribe_forms/form_copyright_association.js b/js/components/ascribe_forms/form_copyright_association.js index c378ddba..124a980a 100644 --- a/js/components/ascribe_forms/form_copyright_association.js +++ b/js/components/ascribe_forms/form_copyright_association.js @@ -48,8 +48,7 @@ let CopyrightAssociationForm = React.createClass({ + label={getLangText('Copyright Association')}> {this.props.orderParams.map((param) => { return ( -
-
  • -
    - - {getLangText(param.replace('_', ' '))} - - -1} /> -
    -
  • -
    +
  • +
    + + {getLangText(param.replace('_', ' '))} + + -1} /> +
    +
  • ); })} @@ -89,4 +87,4 @@ let PieceListToolbarOrderWidget = React.createClass({ } }); -export default PieceListToolbarOrderWidget; \ No newline at end of file +export default PieceListToolbarOrderWidget; diff --git a/js/components/ascribe_routes/proxy_handler.js b/js/components/ascribe_routes/proxy_handler.js index 006d4e32..55987ee5 100644 --- a/js/components/ascribe_routes/proxy_handler.js +++ b/js/components/ascribe_routes/proxy_handler.js @@ -20,8 +20,8 @@ const WHEN_ENUM = ['loggedIn', 'loggedOut']; export function AuthRedirect({to, when}) { // validate `when`, must be contained in `WHEN_ENUM`. // Throw an error otherwise. - if(WHEN_ENUM.indexOf(when) === -1) { - let whenValues = WHEN_ENUM.join(', '); + if (WHEN_ENUM.indexOf(when) === -1) { + const whenValues = WHEN_ENUM.join(', '); throw new Error(`"when" must be one of: [${whenValues}] got "${when}" instead`); } @@ -39,14 +39,14 @@ export function AuthRedirect({to, when}) { // and redirect if `true`. if (exprToValidate) { - window.setTimeout(() => history.replaceState(null, to, query)); + window.setTimeout(() => history.replace({ query, pathname: to })); return true; // Otherwise there can also be the case that the backend // wants to redirect the user to a specific route when the user is logged out already } else if (!exprToValidate && when === 'loggedIn' && redirect) { delete query.redirect; - window.setTimeout(() => history.replaceState(null, '/' + redirect, query)); + window.setTimeout(() => history.replace({ query, pathname: '/' + redirect })); return true; } else if (!exprToValidate && when === 'loggedOut' && redirectAuthenticated) { diff --git a/js/components/ascribe_slides_container/slides_container.js b/js/components/ascribe_slides_container/slides_container.js index 39d515a3..109bbae7 100644 --- a/js/components/ascribe_slides_container/slides_container.js +++ b/js/components/ascribe_slides_container/slides_container.js @@ -57,21 +57,21 @@ const SlidesContainer = React.createClass({ // When the start_from parameter is used, this.setSlideNum can not simply be used anymore. nextSlide(additionalQueryParams) { const slideNum = parseInt(this.props.location.query.slide_num, 10) || 0; - let nextSlide = slideNum + 1; - this.setSlideNum(nextSlide, additionalQueryParams); + this.setSlideNum(slideNum + 1, additionalQueryParams); }, setSlideNum(nextSlideNum, additionalQueryParams = {}) { - let queryParams = Object.assign(this.props.location.query, additionalQueryParams); - queryParams.slide_num = nextSlideNum; - this.history.pushState(null, this.props.location.pathname, queryParams); + const { location: { pathname } } = this.props; + const query = Object.assign({}, this.props.location.query, additionalQueryParams, { slide_num: nextSlideNum }); + + this.history.push({ pathname, query }); }, // breadcrumbs are defined as attributes of the slides. // To extract them we have to read the DOM element's attributes extractBreadcrumbs() { const startFrom = parseInt(this.props.location.query.start_from, 10) || -1; - let breadcrumbs = []; + const breadcrumbs = []; React.Children.map(this.props.children, (child, i) => { if(child && i >= startFrom && child.props['data-slide-title']) { @@ -179,4 +179,4 @@ const SlidesContainer = React.createClass({ } }); -export default SlidesContainer; \ No newline at end of file +export default SlidesContainer; diff --git a/js/components/ascribe_table/models/table_models.js b/js/components/ascribe_table/models/table_models.js index b675d14e..99cf7e64 100644 --- a/js/components/ascribe_table/models/table_models.js +++ b/js/components/ascribe_table/models/table_models.js @@ -2,15 +2,15 @@ export class ColumnModel { // ToDo: Add validation for all passed-in parameters - constructor(transformFn, columnName, displayName, displayType, rowWidth, canBeOrdered, transition, className) { + constructor({ transformFn, columnName = '', displayElement, displayType, rowWidth, canBeOrdered, transition, className = '' }) { this.transformFn = transformFn; this.columnName = columnName; - this.displayName = displayName; + this.displayElement = displayElement; this.displayType = displayType; this.rowWidth = rowWidth; this.canBeOrdered = canBeOrdered; this.transition = transition; - this.className = className ? className : ''; + this.className = className; } } @@ -28,7 +28,7 @@ export class ColumnModel { * our selfes, using this TransitionModel. */ export class TransitionModel { - constructor(to, queryKey, valueKey, callback) { + constructor({ to, queryKey, valueKey, callback }) { this.to = to; this.queryKey = queryKey; this.valueKey = valueKey; @@ -38,4 +38,4 @@ export class TransitionModel { toReactRouterLink(queryValue) { return '/' + this.to + '/' + queryValue; } -} \ No newline at end of file +} diff --git a/js/components/ascribe_table/table_header.js b/js/components/ascribe_table/table_header.js index f807627b..78a31681 100644 --- a/js/components/ascribe_table/table_header.js +++ b/js/components/ascribe_table/table_header.js @@ -1,5 +1,5 @@ -'use strict'; +'use strict'; import React from 'react'; import TableHeaderItem from './table_header_item'; @@ -29,7 +29,7 @@ let TableHeader = React.createClass({ - {this.props.displayName} + {displayElement} ); } else { return ( - {this.props.displayName} + {displayElement} ); } } else { return ( - + - {this.props.displayName} + {displayElement} ); diff --git a/js/components/ascribe_table/table_item_acl_filtered.js b/js/components/ascribe_table/table_item_acl_filtered.js index 22a28130..9ca91bcf 100644 --- a/js/components/ascribe_table/table_item_acl_filtered.js +++ b/js/components/ascribe_table/table_item_acl_filtered.js @@ -3,15 +3,15 @@ import React from 'react'; -let TableItemAclFiltered = React.createClass({ +const TableItemAclFiltered = React.createClass({ propTypes: { content: React.PropTypes.object, - notifications: React.PropTypes.string + notifications: React.PropTypes.array }, render() { - var availableAcls = ['acl_consign', 'acl_loan', 'acl_transfer', 'acl_view', 'acl_share', 'acl_unshare', 'acl_delete']; - if (this.props.notifications && this.props.notifications.length > 0){ + const availableAcls = ['acl_consign', 'acl_loan', 'acl_transfer', 'acl_view', 'acl_share', 'acl_unshare', 'acl_delete']; + if (this.props.notifications && this.props.notifications.length) { return ( {this.props.notifications[0].action_str} @@ -19,15 +19,14 @@ let TableItemAclFiltered = React.createClass({ ); } - let filteredAcls = Object.keys(this.props.content).filter((key) => { - return availableAcls.indexOf(key) > -1 && this.props.content[key]; - }); - - filteredAcls = filteredAcls.map((acl) => acl.split('acl_')[1]); + const filteredAcls = Object.keys(this.props.content) + .filter((key) => availableAcls.indexOf(key) > -1 && this.props.content[key]) + .map((acl) => acl.split('acl_')[1]) + .join('/'); return ( - {filteredAcls.join('/')} + {filteredAcls} ); } diff --git a/js/components/password_reset_container.js b/js/components/password_reset_container.js index f9ece359..1649479e 100644 --- a/js/components/password_reset_container.js +++ b/js/components/password_reset_container.js @@ -135,7 +135,8 @@ let PasswordResetForm = React.createClass({ }, handleSuccess() { - this.history.pushState(null, '/collection'); + this.history.push('/collection'); + const notification = new GlobalNotificationModel(getLangText('password successfully updated'), 'success', 10000); GlobalNotificationActions.appendGlobalNotification(notification); }, diff --git a/js/components/piece_list.js b/js/components/piece_list.js index baf965b4..62407534 100644 --- a/js/components/piece_list.js +++ b/js/components/piece_list.js @@ -121,13 +121,13 @@ let PieceList = React.createClass({ }, componentDidUpdate() { - const { redirectTo, shouldRedirect } = this.props; + const { location: { query }, redirectTo, shouldRedirect } = this.props; const { unfilteredPieceListCount } = this.state; if (redirectTo && unfilteredPieceListCount === 0 && (typeof shouldRedirect === 'function' && shouldRedirect(unfilteredPieceListCount))) { // FIXME: hack to redirect out of the dispatch cycle - window.setTimeout(() => this.history.pushState(null, this.props.redirectTo, this.props.location.query), 0); + window.setTimeout(() => this.history.push({ query, pathname: redirectTo }), 0); } }, @@ -180,15 +180,16 @@ let PieceList = React.createClass({ } }, - searchFor(searchTerm) { - this.loadPieceList({ - page: 1, - search: searchTerm - }); - this.history.pushState(null, this.props.location.pathname, {page: 1}); + searchFor(search) { + const { location: { pathname } } = this.props; + + this.loadPieceList({ search, page: 1 }); + this.history.push({ pathname, query: { page: 1 } }); }, - applyFilterBy(filterBy){ + applyFilterBy(filterBy) { + const { location: { pathname } } = this.props; + this.setState({ isFilterDirty: true }); @@ -212,7 +213,7 @@ let PieceList = React.createClass({ // we have to redirect the user always to page one as it could be that there is no page two // for filtered pieces - this.history.pushState(null, this.props.location.pathname, {page: 1}); + this.history.push({ pathname, query: { page: 1 } }); }, applyOrderBy(orderBy) { diff --git a/js/components/register_piece.js b/js/components/register_piece.js index b5fe976b..e855403a 100644 --- a/js/components/register_piece.js +++ b/js/components/register_piece.js @@ -73,7 +73,7 @@ let RegisterPiece = React.createClass( { this.state.filterBy ); - this.history.pushState(null, `/pieces/${response.piece.id}`); + this.history.push(`/pieces/${response.piece.id}`); }, getSpecifyEditions() { diff --git a/js/components/whitelabel/prize/portfolioreview/components/pr_forms/pr_register_piece_form.js b/js/components/whitelabel/prize/portfolioreview/components/pr_forms/pr_register_piece_form.js index 2f5954ac..e4bcb810 100644 --- a/js/components/whitelabel/prize/portfolioreview/components/pr_forms/pr_register_piece_form.js +++ b/js/components/whitelabel/prize/portfolioreview/components/pr_forms/pr_register_piece_form.js @@ -105,7 +105,7 @@ const PRRegisterPieceForm = React.createClass({ GlobalNotificationActions.appendGlobalNotification(notificationMessage); }); }) - .then(() => this.history.pushState(null, `/pieces/${this.state.piece.id}`)) + .then(() => this.history.push(`/pieces/${this.state.piece.id}`)) .catch((err) => { const errMessage = (getErrorNotificationMessage(err) || getLangText("Oops! We weren't able to send your submission.")) + getLangText(' Please contact support@ascribe.io'); diff --git a/js/components/whitelabel/prize/portfolioreview/components/pr_landing.js b/js/components/whitelabel/prize/portfolioreview/components/pr_landing.js index 039a4b45..2ac1d19a 100644 --- a/js/components/whitelabel/prize/portfolioreview/components/pr_landing.js +++ b/js/components/whitelabel/prize/portfolioreview/components/pr_landing.js @@ -11,6 +11,7 @@ import LinkContainer from 'react-router-bootstrap/lib/LinkContainer'; import PrizeActions from '../../simple_prize/actions/prize_actions'; import PrizeStore from '../../simple_prize/stores/prize_store'; +import { omitFromObject } from '../../../../../utils/general_utils'; import { getLangText } from '../../../../../utils/lang_utils'; @@ -34,12 +35,15 @@ const PRLanding = React.createClass({ const { location } = this.props; PrizeStore.listen(this.onChange); + + UserActions.fetchCurrentUser(); PrizeActions.fetchPrize(); if (location.query.redirect) { - let queryCopy = JSON.parse(JSON.stringify(location.query)); - delete queryCopy.redirect; - window.setTimeout(() => this.history.replaceState(null, `/${location.query.redirect}`, queryCopy)); + window.setTimeout(() => this.history.replace({ + pathname: `/${location.query.redirect}`, + query: omitFromObject(location.query, ['redirect']) + })); } }, diff --git a/js/components/whitelabel/prize/portfolioreview/components/pr_register_piece.js b/js/components/whitelabel/prize/portfolioreview/components/pr_register_piece.js index 8f0479dc..46c34f6c 100644 --- a/js/components/whitelabel/prize/portfolioreview/components/pr_register_piece.js +++ b/js/components/whitelabel/prize/portfolioreview/components/pr_register_piece.js @@ -32,7 +32,7 @@ const PRRegisterPiece = React.createClass({ if (currentUser && currentUser.email) { const submittedPieceId = getCookie(currentUser.email); if (submittedPieceId) { - this.history.pushState(null, `/pieces/${submittedPieceId}`); + this.history.push(`/pieces/${submittedPieceId}`); } } }, diff --git a/js/components/whitelabel/prize/portfolioreview/components/pr_routes/pr_proxy_handler.js b/js/components/whitelabel/prize/portfolioreview/components/pr_routes/pr_proxy_handler.js index 04ff8ce6..f81c4539 100644 --- a/js/components/whitelabel/prize/portfolioreview/components/pr_routes/pr_proxy_handler.js +++ b/js/components/whitelabel/prize/portfolioreview/components/pr_routes/pr_proxy_handler.js @@ -17,7 +17,7 @@ export function AuthPrizeRoleRedirect({ to, when }) { .reduce((a, b) => a || b); if (exprToValidate) { - window.setTimeout(() => history.replaceState(null, to, query)); + window.setTimeout(() => history.replace({ query, pathname: to })); return true; } else { return false; diff --git a/js/components/whitelabel/wallet/components/ascribe_detail/wallet_piece_container.js b/js/components/whitelabel/wallet/components/ascribe_detail/wallet_piece_container.js index 5c9af8b1..3b207776 100644 --- a/js/components/whitelabel/wallet/components/ascribe_detail/wallet_piece_container.js +++ b/js/components/whitelabel/wallet/components/ascribe_detail/wallet_piece_container.js @@ -33,53 +33,61 @@ let WalletPieceContainer = React.createClass({ }, render() { - if (this.props.piece && this.props.piece.id) { + const { + children, + currentUser, + handleDeleteSuccess, + loadPiece, + piece, + submitButtonType } = this.props; + + if (piece && piece.id) { return (
    -

    {this.props.piece.title}

    - - +

    {piece.title}

    + +
    } subheader={
    - - + +
    }> + piece={piece} + currentUser={currentUser} + loadPiece={loadPiece} + handleDeleteSuccess={handleDeleteSuccess} + submitButtonType={submitButtonType}/> 0}> + show={piece.loan_history && piece.loan_history.length > 0}> + history={piece.loan_history}/> + show={!!(currentUser.username || piece.public_note)}> {return {'id': this.props.piece.id}; }} + id={() => {return {'id': piece.id}; }} label={getLangText('Personal note (private)')} - defaultValue={this.props.piece.private_note || null} + defaultValue={piece.private_note || null} placeholder={getLangText('Enter your comments ...')} editable={true} successMessage={getLangText('Private note saved')} url={ApiUrls.note_private_piece} - currentUser={this.props.currentUser}/> + currentUser={currentUser}/> - {this.props.children} + {children}
    ); } else { diff --git a/js/components/whitelabel/wallet/components/cyland/cyland_detail/cyland_piece_container.js b/js/components/whitelabel/wallet/components/cyland/cyland_detail/cyland_piece_container.js index 28fc34a5..d0315979 100644 --- a/js/components/whitelabel/wallet/components/cyland/cyland_detail/cyland_piece_container.js +++ b/js/components/whitelabel/wallet/components/cyland/cyland_detail/cyland_piece_container.js @@ -86,7 +86,7 @@ let CylandPieceContainer = React.createClass({ const notification = new GlobalNotificationModel(response.notification, 'success'); GlobalNotificationActions.appendGlobalNotification(notification); - this.history.pushState(null, '/collection'); + this.history.push('/collection'); }, render() { diff --git a/js/components/whitelabel/wallet/components/cyland/cyland_register_piece.js b/js/components/whitelabel/wallet/components/cyland/cyland_register_piece.js index c5370698..7739a159 100644 --- a/js/components/whitelabel/wallet/components/cyland/cyland_register_piece.js +++ b/js/components/whitelabel/wallet/components/cyland/cyland_register_piece.js @@ -117,7 +117,7 @@ let CylandRegisterPiece = React.createClass({ PieceActions.fetchOne(this.state.piece.id); - this.history.pushState(null, `/pieces/${this.state.piece.id}`); + this.history.push(`/pieces/${this.state.piece.id}`); }, // We need to increase the step to lock the forms that are already filled out diff --git a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_contract_notifications.js b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_contract_notifications.js index 2bbe15e4..b2921179 100644 --- a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_contract_notifications.js +++ b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_contract_notifications.js @@ -115,7 +115,7 @@ let IkonotvContractNotifications = React.createClass({ NotificationActions.flushContractAgreementListNotifications(); NotificationActions.fetchContractAgreementListNotifications(); - this.history.pushState(null, '/collection'); + this.history.push('/collection'); }, handleDeny() { @@ -129,7 +129,7 @@ let IkonotvContractNotifications = React.createClass({ const notification = new GlobalNotificationModel(getLangText('You have denied the conditions'), 'success', 5000); GlobalNotificationActions.appendGlobalNotification(notification); - this.history.pushState(null, '/collection'); + this.history.push('/collection'); }, getCopyrightAssociationForm(){ diff --git a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_detail/ikonotv_piece_container.js b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_detail/ikonotv_piece_container.js index 68d5a3e7..68e3c2eb 100644 --- a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_detail/ikonotv_piece_container.js +++ b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_detail/ikonotv_piece_container.js @@ -95,7 +95,7 @@ let IkonotvPieceContainer = React.createClass({ const notification = new GlobalNotificationModel(response.notification, 'success'); GlobalNotificationActions.appendGlobalNotification(notification); - this.history.pushState(null, '/collection'); + this.history.push('/collection'); }, render() { diff --git a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_register_piece.js b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_register_piece.js index d72080a7..20a82db2 100644 --- a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_register_piece.js +++ b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_register_piece.js @@ -94,7 +94,7 @@ let IkonotvRegisterPiece = React.createClass({ PieceActions.updatePiece(response.piece); } if (!this.canSubmit()) { - this.history.pushState(null, '/collection'); + this.history.push('/collection'); } else { this.incrementStep(); @@ -125,7 +125,7 @@ let IkonotvRegisterPiece = React.createClass({ this.refreshPieceList(); PieceActions.fetchOne(this.state.piece.id); - this.history.pushState(null, `/pieces/${this.state.piece.id}`); + this.history.push(`/pieces/${this.state.piece.id}`); }, // We need to increase the step to lock the forms that are already filled out diff --git a/js/components/whitelabel/wallet/components/market/market_register_piece.js b/js/components/whitelabel/wallet/components/market/market_register_piece.js index 927fed62..88de2026 100644 --- a/js/components/whitelabel/wallet/components/market/market_register_piece.js +++ b/js/components/whitelabel/wallet/components/market/market_register_piece.js @@ -74,7 +74,7 @@ let MarketRegisterPiece = React.createClass({ handleAdditionalDataSuccess() { this.refreshPieceList(); - this.history.pushState(null, '/collection'); + this.history.push('/collection'); }, // We need to increase the step to lock the forms that are already filled out diff --git a/js/history.js b/js/history.js index 903f2b73..4e8c03c7 100644 --- a/js/history.js +++ b/js/history.js @@ -1,6 +1,7 @@ 'use strict'; import useBasename from 'history/lib/useBasename'; +import useQueries from 'history/lib/useQueries'; import createBrowserHistory from 'history/lib/createBrowserHistory'; import AppConstants from './constants/application_constants'; @@ -8,6 +9,6 @@ import AppConstants from './constants/application_constants'; // Remove the trailing slash if present let baseUrl = AppConstants.baseUrl.replace(/\/$/, ''); -export default useBasename(createBrowserHistory)({ +export default useBasename(useQueries(createBrowserHistory))({ basename: baseUrl }); diff --git a/js/third_party/notifications.js b/js/third_party/notifications.js index 9e33cdaf..8a3f478a 100644 --- a/js/third_party/notifications.js +++ b/js/third_party/notifications.js @@ -30,7 +30,7 @@ class NotificationsHandler { console.log('Contractagreement notifications loaded'); this.loaded = true; - history.pushState(null, '/contract_notifications'); + history.push('/contract_notifications'); } } ); diff --git a/js/utils/url_utils.js b/js/utils/url_utils.js index 86c8dfc5..94584026 100644 --- a/js/utils/url_utils.js +++ b/js/utils/url_utils.js @@ -2,7 +2,7 @@ import camelCase from 'camelcase'; import decamelize from 'decamelize'; -import qs from 'qs'; +import queryString from 'query-string'; import { sanitize } from './general_utils'; @@ -36,8 +36,7 @@ export function argsToQueryParams(obj) { queryParamObj[decamelize(key)] = sanitizedObj[key]; }); - // Use bracket arrayFormat as history.js and react-router use it - return '?' + qs.stringify(queryParamObj, { arrayFormat: 'brackets' }); + return '?' + queryString.stringify(queryParamObj); } /** @@ -56,13 +55,13 @@ export function getCurrentQueryParams() { * @return {object} Query params dictionary */ export function queryParamsToArgs(queryParamString) { - const qsQueryParamObj = qs.parse(queryParamString); + const queryParamObj = queryString.parse(queryParamString); const camelCaseParamObj = {}; Object - .keys(qsQueryParamObj) + .keys(queryParamObj) .forEach((key) => { - camelCaseParamObj[camelCase(key)] = qsQueryParamObj[key]; + camelCaseParamObj[camelCase(key)] = queryParamObj[key]; }); return camelCaseParamObj; diff --git a/package.json b/package.json index 13bdfd46..ee1456fb 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "gulp-uglify": "^1.2.0", "gulp-util": "^3.0.4", "harmonize": "^1.4.2", - "history": "^1.13.1", + "history": "1.17.0", "invariant": "^2.1.1", "isomorphic-fetch": "^2.0.2", "jest-cli": "^0.4.0", @@ -76,12 +76,12 @@ "object-assign": "^2.0.0", "opn": "^3.0.2", "q": "^1.4.1", - "qs": "^4.0.0", + "query-string": "^3.0.0", "raven-js": "^1.1.19", "react": "0.13.2", "react-bootstrap": "0.25.1", "react-datepicker": "^0.12.0", - "react-router": "1.0.0", + "react-router": "1.0.3", "react-router-bootstrap": "^0.19.0", "react-star-rating": "~1.3.2", "react-textarea-autosize": "^2.5.2", From eafa675760a142f9bddba37a800ef9ef7255ec0b Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Mon, 11 Jan 2016 17:57:35 +0100 Subject: [PATCH 16/94] Small code style changes --- .../ascribe_detail/media_container.js | 20 +++++++------ .../ascribe_detail/piece_container.js | 28 ++++++++----------- 2 files changed, 23 insertions(+), 25 deletions(-) diff --git a/js/components/ascribe_detail/media_container.js b/js/components/ascribe_detail/media_container.js index e4270132..e8d9d34e 100644 --- a/js/components/ascribe_detail/media_container.js +++ b/js/components/ascribe_detail/media_container.js @@ -23,9 +23,10 @@ const EMBED_IFRAME_HEIGHT = { let MediaContainer = React.createClass({ propTypes: { - content: React.PropTypes.object, - currentUser: React.PropTypes.object, - refreshObject: React.PropTypes.func + content: React.PropTypes.object.isRequired, + refreshObject: React.PropTypes.func.isRequired, + + currentUser: React.PropTypes.object }, getInitialState() { @@ -35,13 +36,14 @@ let MediaContainer = React.createClass({ }, componentDidMount() { - if (!this.props.content.digital_work) { + const { content, refreshObject } = this.props; + if (!content.digital_work) { return; } - const isEncoding = this.props.content.digital_work.isEncoding; - if (this.props.content.digital_work.mime === 'video' && typeof isEncoding === 'number' && isEncoding !== 100 && !this.state.timerId) { - let timerId = window.setInterval(this.props.refreshObject, 10000); + const isEncoding = content.digital_work.isEncoding; + if (content.digital_work.mime === 'video' && typeof isEncoding === 'number' && isEncoding !== 100 && !this.state.timerId) { + const timerId = window.setInterval(refreshObject, 10000); this.setState({timerId: timerId}); } }, @@ -93,7 +95,7 @@ let MediaContainer = React.createClass({ {''} - }/> + } /> ); } return ( @@ -120,7 +122,7 @@ let MediaContainer = React.createClass({ className="ascribe-margin-1px" href={content.digital_work.url} target="_blank"> - {getLangText('Download')} .{mimetype} + {getLangText('Download')} .{mimetype} {embed} diff --git a/js/components/ascribe_detail/piece_container.js b/js/components/ascribe_detail/piece_container.js index 8d63e31f..93bbd90b 100644 --- a/js/components/ascribe_detail/piece_container.js +++ b/js/components/ascribe_detail/piece_container.js @@ -7,39 +7,35 @@ import Moment from 'moment'; import ReactError from '../../mixins/react_error'; import { ResourceNotFoundError } from '../../models/errors'; +import EditionListActions from '../../actions/edition_list_actions'; + +import GlobalNotificationModel from '../../models/global_notification_model'; +import GlobalNotificationActions from '../../actions/global_notification_actions'; + import PieceActions from '../../actions/piece_actions'; import PieceStore from '../../stores/piece_store'; import PieceListActions from '../../actions/piece_list_actions'; import PieceListStore from '../../stores/piece_list_store'; -import UserActions from '../../actions/user_actions'; -import UserStore from '../../stores/user_store'; - -import EditionListActions from '../../actions/edition_list_actions'; - -import Piece from './piece'; -import CollapsibleParagraph from './../ascribe_collapsible/collapsible_paragraph'; import FurtherDetails from './further_details'; - import DetailProperty from './detail_property'; -import LicenseDetail from './license_detail'; import HistoryIterator from './history_iterator'; +import LicenseDetail from './license_detail'; +import Note from './note'; +import Piece from './piece'; import AclButtonList from './../ascribe_buttons/acl_button_list'; -import CreateEditionsForm from '../ascribe_forms/create_editions_form'; +import AclInformation from '../ascribe_buttons/acl_information'; import CreateEditionsButton from '../ascribe_buttons/create_editions_button'; import DeleteButton from '../ascribe_buttons/delete_button'; -import AclInformation from '../ascribe_buttons/acl_information'; -import AclProxy from '../acl_proxy'; +import CollapsibleParagraph from './../ascribe_collapsible/collapsible_paragraph'; +import CreateEditionsForm from '../ascribe_forms/create_editions_form'; import ListRequestActions from '../ascribe_forms/list_form_request_actions'; -import GlobalNotificationModel from '../../models/global_notification_model'; -import GlobalNotificationActions from '../../actions/global_notification_actions'; - -import Note from './note'; +import AclProxy from '../acl_proxy'; import ApiUrls from '../../constants/api_urls'; import AscribeSpinner from '../ascribe_spinner'; From 58f57af932c44d3555c32245c225ce25ca54e653 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Mon, 11 Jan 2016 18:07:52 +0100 Subject: [PATCH 17/94] Remove unused props in LoginForm --- js/components/ascribe_forms/form_login.js | 41 +++++-------------- js/components/login_container.js | 20 +-------- .../components/prize_login_container.js | 7 ++-- 3 files changed, 15 insertions(+), 53 deletions(-) diff --git a/js/components/ascribe_forms/form_login.js b/js/components/ascribe_forms/form_login.js index a604850d..04292bff 100644 --- a/js/components/ascribe_forms/form_login.js +++ b/js/components/ascribe_forms/form_login.js @@ -6,7 +6,6 @@ import { History } from 'react-router'; import GlobalNotificationModel from '../../models/global_notification_model'; import GlobalNotificationActions from '../../actions/global_notification_actions'; -import UserStore from '../../stores/user_store'; import UserActions from '../../actions/user_actions'; import Form from './form'; @@ -23,8 +22,6 @@ let LoginForm = React.createClass({ propTypes: { headerMessage: React.PropTypes.string, submitMessage: React.PropTypes.string, - redirectOnLoggedIn: React.PropTypes.bool, - redirectOnLoginSuccess: React.PropTypes.bool, location: React.PropTypes.object }, @@ -32,40 +29,24 @@ let LoginForm = React.createClass({ getDefaultProps() { return { - headerMessage: getLangText('Enter ascribe'), - submitMessage: getLangText('Log in'), - redirectOnLoggedIn: true, - redirectOnLoginSuccess: true + headerMessage: getLangText('Enter') + ' ascribe', + submitMessage: getLangText('Log in') }; }, - getInitialState() { - return UserStore.getState(); - }, - - componentDidMount() { - UserStore.listen(this.onChange); - }, - - componentWillUnmount() { - UserStore.unlisten(this.onChange); - }, - - onChange(state) { - this.setState(state); - }, - - handleSuccess({ success }){ - let notification = new GlobalNotificationModel('Login successful', 'success', 10000); + handleSuccess({ success }) { + const notification = new GlobalNotificationModel('Login successful', 'success', 10000); GlobalNotificationActions.appendGlobalNotification(notification); - if(success) { + if (success) { UserActions.fetchCurrentUser(true); } }, render() { - let email = this.props.location.query.email || null; + const { headerMessage, submitMessage, location: { query: { email: emailQuery } } } = this.props; + console.log(emailQuery); + return (
    - {this.props.submitMessage} + {submitMessage} } spinner={ @@ -85,7 +66,7 @@ let LoginForm = React.createClass({ }>
    -

    {this.props.headerMessage}

    +

    {headerMessage}

    - +
    {getLangText('Not an ascribe user')}? {getLangText('Sign up')}...
    {getLangText('Forgot my password')}? {getLangText('Rescue me')}... diff --git a/js/components/whitelabel/prize/simple_prize/components/prize_login_container.js b/js/components/whitelabel/prize/simple_prize/components/prize_login_container.js index 7dd25bea..62ef5adb 100644 --- a/js/components/whitelabel/prize/simple_prize/components/prize_login_container.js +++ b/js/components/whitelabel/prize/simple_prize/components/prize_login_container.js @@ -26,12 +26,11 @@ let LoginContainer = React.createClass({
    -
    + location={this.props.location} /> +
    {getLangText('I\'m not a user') + ' '} {getLangText('Sign up...')} -
    +
    {getLangText('I forgot my password') + ' '} {getLangText('Rescue me...')} From 1c4ca0d15218bd80f4ead0de6fa8269e0c495bfc Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Mon, 11 Jan 2016 18:16:46 +0100 Subject: [PATCH 18/94] Change RegisterPieceForm and parents for loading UserStore from top level app --- .../ascribe_forms/form_register_piece.js | 58 +++++++------------ js/components/register_piece.js | 3 +- .../cyland/cyland_register_piece.js | 3 +- .../ikonotv/ikonotv_register_piece.js | 10 ++-- .../market/market_register_piece.js | 2 + 5 files changed, 32 insertions(+), 44 deletions(-) diff --git a/js/components/ascribe_forms/form_register_piece.js b/js/components/ascribe_forms/form_register_piece.js index 596f8a56..35059d5d 100644 --- a/js/components/ascribe_forms/form_register_piece.js +++ b/js/components/ascribe_forms/form_register_piece.js @@ -2,9 +2,6 @@ import React from 'react'; -import UserStore from '../../stores/user_store'; -import UserActions from '../../actions/user_actions'; - import Form from './form'; import Property from './property'; import InputFineUploader from './input_fineuploader'; @@ -16,22 +13,24 @@ import AppConstants from '../../constants/application_constants'; import AscribeSpinner from '../ascribe_spinner'; import { getLangText } from '../../utils/lang_utils'; -import { mergeOptions } from '../../utils/general_utils'; import { formSubmissionValidation } from '../ascribe_uploader/react_s3_fine_uploader_utils'; let RegisterPieceForm = React.createClass({ propTypes: { + currentUser: React.PropTypes.object.isRequired, + headerMessage: React.PropTypes.string, submitMessage: React.PropTypes.string, - handleSuccess: React.PropTypes.func, - isFineUploaderActive: React.PropTypes.bool, - isFineUploaderEditable: React.PropTypes.bool, enableLocalHashing: React.PropTypes.bool, enableSeparateThumbnail: React.PropTypes.bool, + isFineUploaderActive: React.PropTypes.bool, + isFineUploaderEditable: React.PropTypes.bool, + handleSuccess: React.PropTypes.func, // For this form to work with SlideContainer, we sometimes have to disable it disabled: React.PropTypes.bool, + location: React.PropTypes.object, children: React.PropTypes.oneOfType([ React.PropTypes.arrayOf(React.PropTypes.element), @@ -48,26 +47,10 @@ let RegisterPieceForm = React.createClass({ }; }, - getInitialState(){ - return mergeOptions( - { - digitalWorkFile: null - }, - UserStore.getState() - ); - }, - - componentDidMount() { - UserStore.listen(this.onChange); - UserActions.fetchCurrentUser(); - }, - - componentWillUnmount() { - UserStore.unlisten(this.onChange); - }, - - onChange(state) { - this.setState(state); + getInitialState() { + return { + digitalWorkFile: null + } }, /** @@ -129,16 +112,17 @@ let RegisterPieceForm = React.createClass({ }, render() { - const { disabled, - handleSuccess, - submitMessage, - headerMessage, - isFineUploaderActive, - isFineUploaderEditable, - location, - children, - enableLocalHashing } = this.props; - const { currentUser} = this.state; + const { + children, + currentUser, + disabled, + enableLocalHashing, + handleSuccess, + headerMessage, + isFineUploaderActive, + isFineUploaderEditable, + location, + submitMessage } = this.props; const profileHashLocally = currentUser && currentUser.profile ? currentUser.profile.hash_locally : false; const hashLocally = profileHashLocally && enableLocalHashing; diff --git a/js/components/register_piece.js b/js/components/register_piece.js index e855403a..49f191c7 100644 --- a/js/components/register_piece.js +++ b/js/components/register_piece.js @@ -102,8 +102,7 @@ let RegisterPiece = React.createClass( { + handleSuccess={this.handleSuccess}> {this.props.children} {this.getSpecifyEditions()} diff --git a/js/components/whitelabel/wallet/components/cyland/cyland_register_piece.js b/js/components/whitelabel/wallet/components/cyland/cyland_register_piece.js index 7739a159..111f1290 100644 --- a/js/components/whitelabel/wallet/components/cyland/cyland_register_piece.js +++ b/js/components/whitelabel/wallet/components/cyland/cyland_register_piece.js @@ -183,13 +183,14 @@ let CylandRegisterPiece = React.createClass({ 0} enableLocalHashing={false} headerMessage={getLangText('Submit to Cyland Archive')} submitMessage={getLangText('Submit')} isFineUploaderActive={true} handleSuccess={this.handleRegisterSuccess} - location={location}/> + location={location} />
    diff --git a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_register_piece.js b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_register_piece.js index 20a82db2..cd11445a 100644 --- a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_register_piece.js +++ b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_register_piece.js @@ -219,7 +219,8 @@ let IkonotvRegisterPiece = React.createClass({ }, render() { - const { pageExitWarning } = this.state; + const { currentUser, location } = this.props; + const { pageExitWarning, step } = this.state; return (
    0} + currentUser={currentUser} + disabled={step > 0} enableLocalHashing={false} headerMessage={getLangText('Register work')} submitMessage={getLangText('Register')} isFineUploaderActive={true} handleSuccess={this.handleRegisterSuccess} - location={this.props.location} /> + location={location} />
    diff --git a/js/components/whitelabel/wallet/components/market/market_register_piece.js b/js/components/whitelabel/wallet/components/market/market_register_piece.js index 88de2026..381835e2 100644 --- a/js/components/whitelabel/wallet/components/market/market_register_piece.js +++ b/js/components/whitelabel/wallet/components/market/market_register_piece.js @@ -107,6 +107,7 @@ let MarketRegisterPiece = React.createClass({ render() { const { + currentUser, location, whitelabel: { name: whitelabelName = 'Market' @@ -128,6 +129,7 @@ let MarketRegisterPiece = React.createClass({ 0} enableLocalHashing={false} headerMessage={getLangText('Consign to %s', whitelabelName)} From d867a3d9c118d618903e06ccc433b66d7b6855dc Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Mon, 11 Jan 2016 18:24:55 +0100 Subject: [PATCH 19/94] Change SignupForm for loading UserStore from top level app --- js/components/ascribe_forms/form_login.js | 6 ++- js/components/ascribe_forms/form_signup.js | 60 +++++++++------------- 2 files changed, 28 insertions(+), 38 deletions(-) diff --git a/js/components/ascribe_forms/form_login.js b/js/components/ascribe_forms/form_login.js index 04292bff..86d94456 100644 --- a/js/components/ascribe_forms/form_login.js +++ b/js/components/ascribe_forms/form_login.js @@ -44,8 +44,10 @@ let LoginForm = React.createClass({ }, render() { - const { headerMessage, submitMessage, location: { query: { email: emailQuery } } } = this.props; - console.log(emailQuery); + const { + headerMessage, + location: { query: { email: emailQuery } }, + submitMessage } = this.props; return ( - {this.props.submitMessage} - } + {submitMessage} + + } spinner={ - }> + }>
    -

    {this.props.headerMessage}

    +

    {headerMessage}

    - {this.props.children} + {children} Date: Mon, 11 Jan 2016 18:26:39 +0100 Subject: [PATCH 20/94] Remove previously missed imports from ContractSettings --- js/components/ascribe_settings/contract_settings.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/js/components/ascribe_settings/contract_settings.js b/js/components/ascribe_settings/contract_settings.js index 500f1811..e825e4b6 100644 --- a/js/components/ascribe_settings/contract_settings.js +++ b/js/components/ascribe_settings/contract_settings.js @@ -8,12 +8,6 @@ import CreateContractForm from '../ascribe_forms/form_create_contract'; import ContractListStore from '../../stores/contract_list_store'; import ContractListActions from '../../actions/contract_list_actions'; -import UserStore from '../../stores/user_store'; -import UserActions from '../../actions/user_actions'; - -import WhitelabelStore from '../../stores/whitelabel_store'; -import WhitelabelActions from '../../actions/whitelabel_actions'; - import ActionPanel from '../ascribe_panel/action_panel'; import ContractSettingsUpdateButton from './contract_settings_update_button'; From d4064ee83ac007bad7089aeb21eae159c27f3ff4 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Mon, 11 Jan 2016 18:41:07 +0100 Subject: [PATCH 21/94] Change Header and parents for loading UserStore from top level app --- js/components/ascribe_app.js | 5 +- js/components/header.js | 95 +++++++------------ .../prize/portfolioreview/pr_app.js | 13 ++- .../prize/simple_prize/prize_app.js | 12 ++- js/components/whitelabel/wallet/wallet_app.js | 10 +- 5 files changed, 62 insertions(+), 73 deletions(-) diff --git a/js/components/ascribe_app.js b/js/components/ascribe_app.js index bf1ac2ec..369bf744 100644 --- a/js/components/ascribe_app.js +++ b/js/components/ascribe_app.js @@ -55,7 +55,10 @@ let AscribeApp = React.createClass({ return (
    -
    +
    diff --git a/js/components/header.js b/js/components/header.js index ed4a9a3d..fcc19e31 100644 --- a/js/components/header.js +++ b/js/components/header.js @@ -14,47 +14,25 @@ import NavItem from 'react-bootstrap/lib/NavItem'; import LinkContainer from 'react-router-bootstrap/lib/LinkContainer'; -import AclProxy from './acl_proxy'; - import EventActions from '../actions/event_actions'; -import UserActions from '../actions/user_actions'; -import UserStore from '../stores/user_store'; - -import WhitelabelActions from '../actions/whitelabel_actions'; -import WhitelabelStore from '../stores/whitelabel_store'; - +import AclProxy from './acl_proxy'; import HeaderNotifications from './header_notification'; - import HeaderNotificationDebug from './header_notification_debug'; - import NavRoutesLinks from './nav_routes_links'; -import { mergeOptions } from '../utils/general_utils'; import { getLangText } from '../utils/lang_utils'; - import { constructHead } from '../utils/dom_utils'; let Header = React.createClass({ propTypes: { - routes: React.PropTypes.arrayOf(React.PropTypes.object) - }, - - getInitialState() { - return mergeOptions( - WhitelabelStore.getState(), - UserStore.getState() - ); + currentUser: React.PropTypes.object.isRequired, + routes: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, + whitelabel: React.PropTypes.object.isRequired }, componentDidMount() { - UserStore.listen(this.onChange); - UserActions.fetchCurrentUser.defer(); - - WhitelabelStore.listen(this.onChange); - WhitelabelActions.fetchWhitelabel.defer(); - // react-bootstrap 0.25.1 has a bug in which it doesn't // close the mobile expanded navigation after a click by itself. // To get rid of this, we set the state of the component ourselves. @@ -62,13 +40,11 @@ let Header = React.createClass({ }, componentWillUnmount() { - UserStore.unlisten(this.onChange); - WhitelabelStore.unlisten(this.onChange); //history.unlisten(this.onRouteChange); }, getLogo() { - const { whitelabel } = this.state; + const { whitelabel } = this.props; if (whitelabel.head) { constructHead(whitelabel.head); @@ -80,19 +56,19 @@ let Header = React.createClass({ Whitelabel brand ); + } else { + return ( + + + + ); } - - return ( - - - - ); }, getPoweredBy() { return (
  • @@ -104,10 +80,6 @@ let Header = React.createClass({ ); }, - onChange(state) { - this.setState(state); - }, - onMenuItemClick() { /* This is a hack to make the dropdown close after clicking on an item @@ -143,59 +115,61 @@ let Header = React.createClass({ }, render() { + const { currentUser, routes } = this.props; let account; let signup; let navRoutesLinks; - if (this.state.currentUser.username){ + + if (currentUser.username) { account = ( + title={currentUser.username}> - + {getLangText('Account Settings')} - + {getLangText('Contract Settings')} - - + + {getLangText('Log out')} ); - navRoutesLinks = ; - } - else { + + navRoutesLinks = ( + + ); + } else { account = ( - + {getLangText('LOGIN')} ); signup = ( - + {getLangText('SIGNUP')} @@ -211,13 +185,12 @@ let Header = React.createClass({ toggleNavKey={0} fixedTop={true} className="hidden-print"> - + diff --git a/js/components/whitelabel/prize/portfolioreview/pr_app.js b/js/components/whitelabel/prize/portfolioreview/pr_app.js index f6364bc8..85091a8b 100644 --- a/js/components/whitelabel/prize/portfolioreview/pr_app.js +++ b/js/components/whitelabel/prize/portfolioreview/pr_app.js @@ -54,8 +54,7 @@ let PRApp = React.createClass({ }, render() { - const { children, history, routes } = this.props; - const { currentUser, whitelabel } = this.state; + const { children, currentUser, history, routes, whitelabel } = this.props; const subdomain = getSubdomain(); // Add the current user and whitelabel settings to all child routes @@ -69,10 +68,16 @@ let PRApp = React.createClass({ let style = {}; let header; if (currentUser && currentUser.email && history.isActive(`/pieces/${getCookie(currentUser.email)}`)) { - header = ; + header = (); style = { paddingTop: '0 !important' }; } else if(currentUser && (currentUser.is_admin || currentUser.is_jury || currentUser.is_judge)) { - header =
    ; + header = ( +
    + ); } else { style = { paddingTop: '0 !important' }; } diff --git a/js/components/whitelabel/prize/simple_prize/prize_app.js b/js/components/whitelabel/prize/simple_prize/prize_app.js index a5cea483..e9083428 100644 --- a/js/components/whitelabel/prize/simple_prize/prize_app.js +++ b/js/components/whitelabel/prize/simple_prize/prize_app.js @@ -53,8 +53,7 @@ let PrizeApp = React.createClass({ }, render() { - const { history, routes, children } = this.props; - const { currentUser, whitelabel } = this.state; + const { children, currentUser, history, routes, whitelabel } = this.props; const subdomain = getSubdomain(); // The second element of routes is always the active component object, where we can @@ -64,9 +63,14 @@ let PrizeApp = React.createClass({ let header = null; // if the path of the current activeRoute is not defined, then this is the IndexRoute if (!path || history.isActive('/login') || history.isActive('/signup')) { - header = ; + header = (); } else { - header =
    ; + header = ( +
    + ); } return ( diff --git a/js/components/whitelabel/wallet/wallet_app.js b/js/components/whitelabel/wallet/wallet_app.js index e35da2da..783bb2ab 100644 --- a/js/components/whitelabel/wallet/wallet_app.js +++ b/js/components/whitelabel/wallet/wallet_app.js @@ -52,8 +52,7 @@ let WalletApp = React.createClass({ }, render() { - const { history, routes, children } = this.props; - const { currentUser, whitelabel } = this.state; + const { children, currentUser, history, routes, whitelabel } = this.props; const subdomain = getSubdomain(); // The second element of routes is always the active component object, where we can @@ -66,7 +65,12 @@ let WalletApp = React.createClass({ && (['cyland', 'ikonotv', 'lumenus', '23vivi']).indexOf(subdomain) > -1) { header = (
    ); } else { - header = (
    ); + header = ( +
    + ); } // In react-router 1.0, Routes have no 'name' property anymore. To keep functionality however, From 80091747874acaa038f244771f314f978f3a757f Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Mon, 11 Jan 2016 19:06:50 +0100 Subject: [PATCH 22/94] Change AccordionListItems and parents for loading UserStore from top level app --- .../accordion_list_item.js | 20 ++-- .../accordion_list_item_piece.js | 5 +- .../accordion_list_item_wallet.js | 7 +- js/components/piece_list.js | 7 +- .../accordion_list_item_prize.js | 99 +++++++++---------- .../cyland_accordion_list_item.js | 55 +++++------ .../ikonotv_accordion_list_item.js | 74 +++++++------- 7 files changed, 127 insertions(+), 140 deletions(-) diff --git a/js/components/ascribe_accordion_list/accordion_list_item.js b/js/components/ascribe_accordion_list/accordion_list_item.js index 38cb77b1..4cc99de0 100644 --- a/js/components/ascribe_accordion_list/accordion_list_item.js +++ b/js/components/ascribe_accordion_list/accordion_list_item.js @@ -21,16 +21,16 @@ let AccordionListItem = React.createClass({ }, render() { - const { linkData, - className, - thumbnail, - heading, - subheading, - subsubheading, - buttons, - badge, - children } = this.props; - + const { + linkData, + className, + thumbnail, + heading, + subheading, + subsubheading, + buttons, + badge, + children } = this.props; return (
    diff --git a/js/components/ascribe_accordion_list/accordion_list_item_piece.js b/js/components/ascribe_accordion_list/accordion_list_item_piece.js index 9f876388..3a21c2ea 100644 --- a/js/components/ascribe_accordion_list/accordion_list_item_piece.js +++ b/js/components/ascribe_accordion_list/accordion_list_item_piece.js @@ -34,11 +34,10 @@ let AccordionListItemPiece = React.createClass({ }, getLinkData() { - let { piece } = this.props; + const { piece } = this.props; - if(piece && piece.first_edition) { + if (piece && piece.first_edition) { return `/editions/${piece.first_edition.bitcoin_id}`; - } else { return `/pieces/${piece.id}`; } diff --git a/js/components/ascribe_accordion_list/accordion_list_item_wallet.js b/js/components/ascribe_accordion_list/accordion_list_item_wallet.js index a8cab166..638e9bb3 100644 --- a/js/components/ascribe_accordion_list/accordion_list_item_wallet.js +++ b/js/components/ascribe_accordion_list/accordion_list_item_wallet.js @@ -29,8 +29,9 @@ import { mergeOptions } from '../../utils/general_utils'; let AccordionListItemWallet = React.createClass({ propTypes: { + content: React.PropTypes.object.isRequired, + className: React.PropTypes.string, - content: React.PropTypes.object, thumbnailPlaceholder: React.PropTypes.func, children: React.PropTypes.oneOfType([ React.PropTypes.arrayOf(React.PropTypes.element), @@ -62,8 +63,8 @@ let AccordionListItemWallet = React.createClass({ this.setState(state); }, - getGlyphicon(){ - if ((this.props.content.notifications && this.props.content.notifications.length > 0)){ + getGlyphicon() { + if (this.props.content.notifications && this.props.content.notifications.length){ return ( - {this.state.pieceList.map((piece, i) => { + {this.state.pieceList.map((piece) => { return ( + currentUser={currentUser} + thumbnailPlaceholder={customThumbnailPlaceholder}> diff --git a/js/components/whitelabel/prize/simple_prize/components/ascribe_accordion_list/accordion_list_item_prize.js b/js/components/whitelabel/prize/simple_prize/components/ascribe_accordion_list/accordion_list_item_prize.js index 3fc3f5fd..4ab94091 100644 --- a/js/components/whitelabel/prize/simple_prize/components/ascribe_accordion_list/accordion_list_item_prize.js +++ b/js/components/whitelabel/prize/simple_prize/components/ascribe_accordion_list/accordion_list_item_prize.js @@ -10,8 +10,6 @@ import PieceListStore from '../../../../../../stores/piece_list_store'; import PrizeRatingActions from '../../actions/prize_rating_actions'; -import UserStore from '../../../../../../stores/user_store'; - import InputCheckbox from '../../../../../ascribe_forms/input_checkbox'; import AccordionListItemPiece from '../../../../../ascribe_accordion_list/accordion_list_item_piece'; @@ -23,13 +21,14 @@ import AclProxy from '../../../../../acl_proxy'; import SubmitToPrizeButton from './../ascribe_buttons/submit_to_prize_button'; import { getLangText } from '../../../../../../utils/lang_utils'; -import { mergeOptions } from '../../../../../../utils/general_utils'; let AccordionListItemPrize = React.createClass({ propTypes: { + content: React.PropTypes.object.isRequired, + currentUser: React.PropTypes.object.isRequired, + className: React.PropTypes.string, - content: React.PropTypes.object, children: React.PropTypes.oneOfType([ React.PropTypes.arrayOf(React.PropTypes.element), React.PropTypes.element @@ -37,20 +36,15 @@ let AccordionListItemPrize = React.createClass({ }, getInitialState() { - return mergeOptions( - PieceListStore.getState(), - UserStore.getState() - ); + return PieceListStore.getState(); }, componentDidMount() { PieceListStore.listen(this.onChange); - UserStore.listen(this.onChange); }, componentWillUnmount() { PieceListStore.unlisten(this.onChange); - UserStore.unlisten(this.onChange); }, onChange(state) { @@ -61,29 +55,30 @@ let AccordionListItemPrize = React.createClass({ PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, this.state.orderBy, this.state.orderAsc, this.state.filterBy); - let notification = new GlobalNotificationModel(response.notification, 'success', 10000); + const notification = new GlobalNotificationModel(response.notification, 'success', 10000); GlobalNotificationActions.appendGlobalNotification(notification); }, getPrizeButtons() { - if (this.state.currentUser && this.state.currentUser.is_jury){ - if ((this.props.content.ratings) && - (this.props.content.ratings.rating || this.props.content.ratings.average)){ + const { currentUser, content: { id, ratings } } = this.props; + + if (currentUser && (currentUser.is_jury || currentUser.is_judge)) { + if (ratings && (ratings.rating || ratings.average)) { // jury and rating available - let rating = null, - caption = null; - if (this.props.content.ratings.rating){ - rating = parseInt(this.props.content.ratings.rating, 10); + let rating = null; + let caption = null; + + if (ratings.rating) { + rating = parseInt(ratings.rating, 10); caption = getLangText('Your rating'); - } - else if (this.props.content.ratings.average){ - rating = this.props.content.ratings.average; - caption = getLangText('Average of ' + this.props.content.ratings.num_ratings + ' rating(s)'); + } else if (ratings.average) { + rating = ratings.average; + caption = getLangText('Average of ' + ratings.num_ratings + ' rating(s)'); } return (
    - +
    ); - } - else { - if (this.state.currentUser.is_judge){ + } else { + if (currentUser.is_judge) { return (
    {getLangText('Not rated')}
    ); + } else { + // jury and no rating yet + return ( +
    + + {getLangText('Submit your rating')} + +
    + ); } - // jury and no rating yet - return ( -
    - - {getLangText('Submit your rating')} - -
    - ); } } return this.getPrizeButtonsParticipant(); @@ -132,8 +127,8 @@ let AccordionListItemPrize = React.createClass({ ); }, - handleShortlistSuccess(message){ - let notification = new GlobalNotificationModel(message, 'success', 2000); + handleShortlistSuccess(message) { + const notification = new GlobalNotificationModel(message, 'success', 2000); GlobalNotificationActions.appendGlobalNotification(notification); }, @@ -142,23 +137,17 @@ let AccordionListItemPrize = React.createClass({ this.state.orderBy, this.state.orderAsc, this.state.filterBy); }, - onSelectChange(){ + onSelectChange() { PrizeRatingActions.toggleShortlist(this.props.content.id) - .then( - (res) => { - this.refreshPieceData(); - return res; - }) - .then( - (res) => { - this.handleShortlistSuccess(res.notification); - } - ); + .then((res) => { + this.refreshPieceData(); + this.handleShortlistSuccess(res.notification); + }); }, - getPrizeBadge(){ - if (this.state.currentUser && this.state.currentUser.is_judge) { + getPrizeBadge() { + if (currentUser && currentUser.is_judge) { return ( ); + } else { + return null; } - return null; }, render() { - const { children, className, content } = this.props; - const { currentUser } = this.state; + const { children, className, content, currentUser } = this.props; // Only show the artist name if you are the participant or if you are a judge and the piece is shortlisted - let artistName = ((currentUser.is_jury && !currentUser.is_judge) || (currentUser.is_judge && !content.selected )) ? + const artistName = ((currentUser.is_jury && !currentUser.is_judge) || (currentUser.is_judge && !content.selected )) ?
    + } buttons={this.getPrizeButtons()} badge={this.getPrizeBadge()}> {children} diff --git a/js/components/whitelabel/wallet/components/cyland/cyland_accordion_list/cyland_accordion_list_item.js b/js/components/whitelabel/wallet/components/cyland/cyland_accordion_list/cyland_accordion_list_item.js index 9d88408f..3ed7830e 100644 --- a/js/components/whitelabel/wallet/components/cyland/cyland_accordion_list/cyland_accordion_list_item.js +++ b/js/components/whitelabel/wallet/components/cyland/cyland_accordion_list/cyland_accordion_list_item.js @@ -3,27 +3,26 @@ import React from 'react'; import Moment from 'moment'; -import AccordionListItemPiece from '../../../../../ascribe_accordion_list/accordion_list_item_piece'; +import GlobalNotificationModel from '../../../../../../models/global_notification_model'; +import GlobalNotificationActions from '../../../../../../actions/global_notification_actions'; import PieceListActions from '../../../../../../actions/piece_list_actions'; import PieceListStore from '../../../../../../stores/piece_list_store'; -import UserStore from '../../../../../../stores/user_store'; - -import GlobalNotificationModel from '../../../../../../models/global_notification_model'; -import GlobalNotificationActions from '../../../../../../actions/global_notification_actions'; - import CylandSubmitButton from '../cyland_buttons/cyland_submit_button'; + +import AccordionListItemPiece from '../../../../../ascribe_accordion_list/accordion_list_item_piece'; + import AclProxy from '../../../../../acl_proxy'; import { getLangText } from '../../../../../../utils/lang_utils'; -import { mergeOptions } from '../../../../../../utils/general_utils'; let CylandAccordionListItem = React.createClass({ propTypes: { + content: React.PropTypes.object.isRequired, + className: React.PropTypes.string, - content: React.PropTypes.object, children: React.PropTypes.oneOfType([ React.PropTypes.arrayOf(React.PropTypes.element), React.PropTypes.element @@ -31,20 +30,15 @@ let CylandAccordionListItem = React.createClass({ }, getInitialState() { - return mergeOptions( - PieceListStore.getState(), - UserStore.getState() - ); + return PieceListStore.getState(); }, componentDidMount() { PieceListStore.listen(this.onChange); - UserStore.listen(this.onChange); }, componentWillUnmount() { PieceListStore.unlisten(this.onChange); - UserStore.unlisten(this.onChange); }, onChange(state) { @@ -55,37 +49,39 @@ let CylandAccordionListItem = React.createClass({ PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, this.state.orderBy, this.state.orderAsc, this.state.filterBy); - let notification = new GlobalNotificationModel(response.notification, 'success', 10000); + const notification = new GlobalNotificationModel(response.notification, 'success', 10000); GlobalNotificationActions.appendGlobalNotification(notification); }, getSubmitButtons() { + const { content } = this.props; + return (
    @@ -95,16 +91,19 @@ let CylandAccordionListItem = React.createClass({ }, render() { + const { children, className, content } = this.props; + return ( - {Moment(this.props.content.date_created, 'YYYY-MM-DD').year()} -
    } + {Moment(content.date_created, 'YYYY-MM-DD').year()} +
    + } buttons={this.getSubmitButtons()}> - {this.props.children} + {children} ); } diff --git a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_accordion_list/ikonotv_accordion_list_item.js b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_accordion_list/ikonotv_accordion_list_item.js index ce8cebf5..53f04c41 100644 --- a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_accordion_list/ikonotv_accordion_list_item.js +++ b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_accordion_list/ikonotv_accordion_list_item.js @@ -3,18 +3,16 @@ import React from 'react'; import Moment from 'moment'; -import AccordionListItemPiece from '../../../../../ascribe_accordion_list/accordion_list_item_piece'; +import GlobalNotificationModel from '../../../../../../models/global_notification_model'; +import GlobalNotificationActions from '../../../../../../actions/global_notification_actions'; import PieceListActions from '../../../../../../actions/piece_list_actions'; import PieceListStore from '../../../../../../stores/piece_list_store'; -import UserStore from '../../../../../../stores/user_store'; - -import GlobalNotificationModel from '../../../../../../models/global_notification_model'; -import GlobalNotificationActions from '../../../../../../actions/global_notification_actions'; - import IkonotvSubmitButton from '../ikonotv_buttons/ikonotv_submit_button'; +import AccordionListItemPiece from '../../../../../ascribe_accordion_list/accordion_list_item_piece'; + import AclProxy from '../../../../../acl_proxy'; import { getLangText } from '../../../../../../utils/lang_utils'; @@ -23,8 +21,10 @@ import { mergeOptions } from '../../../../../../utils/general_utils'; let IkonotvAccordionListItem = React.createClass({ propTypes: { + content: React.PropTypes.object.isRequired, + currentUser: React.PropTypes.object.isRequired, + className: React.PropTypes.string, - content: React.PropTypes.object, children: React.PropTypes.oneOfType([ React.PropTypes.arrayOf(React.PropTypes.element), React.PropTypes.element @@ -32,20 +32,15 @@ let IkonotvAccordionListItem = React.createClass({ }, getInitialState() { - return mergeOptions( - PieceListStore.getState(), - UserStore.getState() - ); + return PieceListStore.getState(); }, componentDidMount() { PieceListStore.listen(this.onChange); - UserStore.listen(this.onChange); }, componentWillUnmount() { PieceListStore.unlisten(this.onChange); - UserStore.unlisten(this.onChange); }, onChange(state) { @@ -56,41 +51,43 @@ let IkonotvAccordionListItem = React.createClass({ PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, this.state.orderBy, this.state.orderAsc, this.state.filterBy); - let notification = new GlobalNotificationModel(response.notification, 'success', 10000); + const notification = new GlobalNotificationModel(response.notification, 'success', 10000); GlobalNotificationActions.appendGlobalNotification(notification); }, getSubmitButtons() { + const { content, currentUser } = this.props; + return (
    + piece={content}/> @@ -100,22 +97,21 @@ let IkonotvAccordionListItem = React.createClass({ }, render() { - if(this.props.content) { - return ( - - {Moment(this.props.content.date_created, 'YYYY-MM-DD').year()} -
    } - buttons={this.getSubmitButtons()}> - {this.props.children} - - ); - } else { - return null; - } + const { children, className, content } = this.props; + + return ( + + {Moment(content.date_created, 'YYYY-MM-DD').year()} +
  • + } + buttons={this.getSubmitButtons()}> + {children} + + ); } }); From 4133922f3178065d7d176af45445584b540edb3b Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Mon, 11 Jan 2016 19:10:24 +0100 Subject: [PATCH 23/94] Fix a few small additions/deletions with UserActions --- js/components/ascribe_forms/form_signup.js | 2 ++ .../whitelabel/prize/portfolioreview/components/pr_landing.js | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/js/components/ascribe_forms/form_signup.js b/js/components/ascribe_forms/form_signup.js index 5d4c57aa..69d50005 100644 --- a/js/components/ascribe_forms/form_signup.js +++ b/js/components/ascribe_forms/form_signup.js @@ -6,6 +6,8 @@ import { History } from 'react-router'; import GlobalNotificationModel from '../../models/global_notification_model'; import GlobalNotificationActions from '../../actions/global_notification_actions'; +import UserActions from '../../actions/user_actions'; + import Form from './form'; import Property from './property'; import InputCheckbox from './input_checkbox'; diff --git a/js/components/whitelabel/prize/portfolioreview/components/pr_landing.js b/js/components/whitelabel/prize/portfolioreview/components/pr_landing.js index 2ac1d19a..4cb925a8 100644 --- a/js/components/whitelabel/prize/portfolioreview/components/pr_landing.js +++ b/js/components/whitelabel/prize/portfolioreview/components/pr_landing.js @@ -35,8 +35,6 @@ const PRLanding = React.createClass({ const { location } = this.props; PrizeStore.listen(this.onChange); - - UserActions.fetchCurrentUser(); PrizeActions.fetchPrize(); if (location.query.redirect) { From 40041197d6db5eaf88ece62cdf792dc28b78c027 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Mon, 11 Jan 2016 19:22:25 +0100 Subject: [PATCH 24/94] Update components for loading WhitelabelStore from top level app --- .../accordion_list_item_wallet.js | 31 +++++++-------- .../ascribe_detail/edition_container.js | 9 +++-- js/components/piece_list.js | 10 +++-- js/components/register_piece.js | 3 -- .../cyland_buttons/cyland_submit_button.js | 28 ++----------- .../market_buttons/market_acl_button_list.js | 8 +++- .../market_buttons/market_submit_button.js | 39 +++++++------------ 7 files changed, 50 insertions(+), 78 deletions(-) diff --git a/js/components/ascribe_accordion_list/accordion_list_item_wallet.js b/js/components/ascribe_accordion_list/accordion_list_item_wallet.js index 638e9bb3..6f3139a3 100644 --- a/js/components/ascribe_accordion_list/accordion_list_item_wallet.js +++ b/js/components/ascribe_accordion_list/accordion_list_item_wallet.js @@ -7,20 +7,19 @@ import Glyphicon from 'react-bootstrap/lib/Glyphicon'; import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger'; import Tooltip from 'react-bootstrap/lib/Tooltip'; -import AccordionListItemPiece from './accordion_list_item_piece'; -import AccordionListItemEditionWidget from './accordion_list_item_edition_widget'; -import CreateEditionsForm from '../ascribe_forms/create_editions_form'; - -import PieceListActions from '../../actions/piece_list_actions'; -import PieceListStore from '../../stores/piece_list_store'; - -import WhitelabelStore from '../../stores/whitelabel_store'; - import EditionListActions from '../../actions/edition_list_actions'; import GlobalNotificationModel from '../../models/global_notification_model'; import GlobalNotificationActions from '../../actions/global_notification_actions'; +import PieceListActions from '../../actions/piece_list_actions'; +import PieceListStore from '../../stores/piece_list_store'; + +import AccordionListItemPiece from './accordion_list_item_piece'; +import AccordionListItemEditionWidget from './accordion_list_item_edition_widget'; +import CreateEditionsForm from '../ascribe_forms/create_editions_form'; + + import AclProxy from '../acl_proxy'; import { getLangText } from '../../utils/lang_utils'; @@ -30,6 +29,7 @@ import { mergeOptions } from '../../utils/general_utils'; let AccordionListItemWallet = React.createClass({ propTypes: { content: React.PropTypes.object.isRequired, + whitelabel: React.PropTypes.object.isRequired, className: React.PropTypes.string, thumbnailPlaceholder: React.PropTypes.func, @@ -41,22 +41,19 @@ let AccordionListItemWallet = React.createClass({ getInitialState() { return mergeOptions( + PieceListStore.getState(), { showCreateEditionsDialog: false - }, - PieceListStore.getState(), - WhitelabelStore.getState() + } ); }, componentDidMount() { PieceListStore.listen(this.onChange); - WhitelabelStore.listen(this.onChange); }, componentWillUnmount() { PieceListStore.unlisten(this.onChange); - WhitelabelStore.unlisten(this.onChange); }, onChange(state) { @@ -64,7 +61,7 @@ let AccordionListItemWallet = React.createClass({ }, getGlyphicon() { - if (this.props.content.notifications && this.props.content.notifications.length){ + if (this.props.content.notifications && this.props.content.notifications.length) { return ( , diff --git a/js/components/ascribe_detail/edition_container.js b/js/components/ascribe_detail/edition_container.js index 00b40ef5..1001be55 100644 --- a/js/components/ascribe_detail/edition_container.js +++ b/js/components/ascribe_detail/edition_container.js @@ -84,8 +84,8 @@ let EditionContainer = React.createClass({ }, render() { + const { actionPanelButtonListType, currentUser, furtherDetailsType, whitelabel } = this.props; const { edition, coaMeta } = this.state; - const { actionPanelButtonListType, currentUser, furtherDetailsType } = this.props; if (Object.keys(edition).length && edition.id) { setDocumentTitle([edition.artist_name, edition.title].join(', ')); @@ -93,11 +93,12 @@ let EditionContainer = React.createClass({ return ( EditionActions.fetchEdition(this.props.params.editionId)} /> + edition={edition} + furtherDetailsType={furtherDetailsType} + loadEdition={() => EditionActions.fetchEdition(this.props.params.editionId)} + whitelabel={whitelabel} /> ); } else { return ( diff --git a/js/components/piece_list.js b/js/components/piece_list.js index 4a4663bb..3a4484ef 100644 --- a/js/components/piece_list.js +++ b/js/components/piece_list.js @@ -268,7 +268,8 @@ let PieceList = React.createClass({ customSubmitButton, customThumbnailPlaceholder, filterParams, - orderParams } = this.props; + orderParams, + whitelabel } = this.props; const loadingElement = ; @@ -276,6 +277,7 @@ let PieceList = React.createClass({ const availableAcls = getAvailableAcls(selectedEditions, (aclName) => aclName !== 'acl_view'); setDocumentTitle(getLangText('Collection')); + return (
    + thumbnailPlaceholder={customThumbnailPlaceholder} + whitelabel={whitelabel}> diff --git a/js/components/register_piece.js b/js/components/register_piece.js index 49f191c7..5ee192aa 100644 --- a/js/components/register_piece.js +++ b/js/components/register_piece.js @@ -6,9 +6,6 @@ import { History } from 'react-router'; import Col from 'react-bootstrap/lib/Col'; import Row from 'react-bootstrap/lib/Row'; -import WhitelabelActions from '../actions/whitelabel_actions'; -import WhitelabelStore from '../stores/whitelabel_store'; - import PieceListStore from '../stores/piece_list_store'; import PieceListActions from '../actions/piece_list_actions'; diff --git a/js/components/whitelabel/wallet/components/cyland/cyland_buttons/cyland_submit_button.js b/js/components/whitelabel/wallet/components/cyland/cyland_buttons/cyland_submit_button.js index 45b8c45e..2a881fbd 100644 --- a/js/components/whitelabel/wallet/components/cyland/cyland_buttons/cyland_submit_button.js +++ b/js/components/whitelabel/wallet/components/cyland/cyland_buttons/cyland_submit_button.js @@ -7,39 +7,19 @@ import Button from 'react-bootstrap/lib/Button'; import LinkContainer from 'react-router-bootstrap/lib/LinkContainer'; -import WhitelabelActions from '../../../../../../actions/whitelabel_actions'; -import WhitelabelStore from '../../../../../../stores/whitelabel_store'; - import { getLangText } from '../../../../../../utils/lang_utils'; let CylandSubmitButton = React.createClass({ propTypes: { - className: React.PropTypes.string, - handleSuccess: React.PropTypes.func, piece: React.PropTypes.object.isRequired, - username: React.PropTypes.string - }, - getInitialState() { - return WhitelabelStore.getState(); - }, - - componentDidMount() { - WhitelabelStore.listen(this.onChange); - WhitelabelActions.fetchWhitelabel(); - }, - - componentWillUnmount() { - WhitelabelStore.unlisten(this.onChange); - }, - - onChange(state) { - this.setState(state); + className: React.PropTypes.string, + handleSuccess: React.PropTypes.func }, render() { - const { piece, className } = this.props; + const { className, piece } = this.props; return ( - + diff --git a/js/components/whitelabel/wallet/components/market/market_buttons/market_submit_button.js b/js/components/whitelabel/wallet/components/market/market_buttons/market_submit_button.js index d8ef4c41..c0f58380 100644 --- a/js/components/whitelabel/wallet/components/market/market_buttons/market_submit_button.js +++ b/js/components/whitelabel/wallet/components/market/market_buttons/market_submit_button.js @@ -3,6 +3,8 @@ import React from 'react'; import classNames from 'classnames'; +import PieceActions from '../../../../../../actions/piece_actions'; + import MarketAdditionalDataForm from '../market_forms/market_additional_data_form'; import AclFormFactory from '../../../../../ascribe_forms/acl_form_factory'; @@ -12,10 +14,6 @@ import ModalWrapper from '../../../../../ascribe_modal/modal_wrapper'; import AclProxy from '../../../../../acl_proxy'; -import PieceActions from '../../../../../../actions/piece_actions'; -import WhitelabelActions from '../../../../../../actions/whitelabel_actions'; -import WhitelabelStore from '../../../../../../stores/whitelabel_store'; - import ApiUrls from '../../../../../../constants/api_urls'; import { getAclFormMessage, getAclFormDataId } from '../../../../../../utils/form_utils'; @@ -24,30 +22,14 @@ import { getLangText } from '../../../../../../utils/lang_utils'; let MarketSubmitButton = React.createClass({ propTypes: { availableAcls: React.PropTypes.object.isRequired, - currentUser: React.PropTypes.object, + currentUser: React.PropTypes.object.isRequired, editions: React.PropTypes.array.isRequired, handleSuccess: React.PropTypes.func.isRequired, + whitelabel: React.PropTypes.object.isRequired, + className: React.PropTypes.string, }, - getInitialState() { - return WhitelabelStore.getState(); - }, - - componentDidMount() { - WhitelabelStore.listen(this.onChange); - - WhitelabelActions.fetchWhitelabel(); - }, - - componentWillUnmount() { - WhitelabelStore.unlisten(this.onChange); - }, - - onChange(state) { - this.setState(state); - }, - canEditionBeSubmitted(edition) { if (edition && edition.extra_data && edition.other_data) { const { extra_data, other_data } = edition; @@ -90,8 +72,14 @@ let MarketSubmitButton = React.createClass({ }, render() { - const { availableAcls, currentUser, className, editions, handleSuccess } = this.props; - const { whitelabel: { name: whitelabelName = 'Market', user: whitelabelAdminEmail } } = this.state; + const { + availableAcls, + currentUser, + className, + editions, + handleSuccess, + whitelabel: { name: whitelabelName = 'Market', user: whitelabelAdminEmail } } = this.props; + const { solePieceId, canSubmit } = this.getAggregateEditionDetails(); const message = getAclFormMessage({ aclName: 'acl_consign', @@ -106,6 +94,7 @@ let MarketSubmitButton = React.createClass({ {getLangText('CONSIGN TO %s', whitelabelName.toUpperCase())} ); + const consignForm = ( Date: Mon, 11 Jan 2016 19:34:39 +0100 Subject: [PATCH 25/94] Fix MediaContainer not being refreshed while in Edition --- js/components/ascribe_detail/edition.js | 35 ++++++++++++++----------- js/components/ascribe_detail/piece.js | 5 ++-- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/js/components/ascribe_detail/edition.js b/js/components/ascribe_detail/edition.js index 803d73bb..46eb8183 100644 --- a/js/components/ascribe_detail/edition.js +++ b/js/components/ascribe_detail/edition.js @@ -8,23 +8,23 @@ import Row from 'react-bootstrap/lib/Row'; import Col from 'react-bootstrap/lib/Col'; import Glyphicon from 'react-bootstrap/lib/Glyphicon'; -import HistoryIterator from './history_iterator'; +import EditionActions from '../../actions/edition_actions'; -import MediaContainer from './media_container'; - -import CollapsibleParagraph from './../ascribe_collapsible/collapsible_paragraph'; - -import Form from './../ascribe_forms/form'; -import Property from './../ascribe_forms/property'; import DetailProperty from './detail_property'; -import LicenseDetail from './license_detail'; -import FurtherDetails from './further_details'; - import EditionActionPanel from './edition_action_panel'; -import AclProxy from '../acl_proxy'; - +import FurtherDetails from './further_details'; +import HistoryIterator from './history_iterator'; +import LicenseDetail from './license_detail'; +import MediaContainer from './media_container'; import Note from './note'; +import CollapsibleParagraph from '../ascribe_collapsible/collapsible_paragraph'; + +import Form from '../ascribe_forms/form'; +import Property from '../ascribe_forms/property'; + +import AclProxy from '../acl_proxy'; + import ApiUrls from '../../constants/api_urls'; import AscribeSpinner from '../ascribe_spinner'; @@ -37,10 +37,10 @@ import { getLangText } from '../../utils/lang_utils'; let Edition = React.createClass({ propTypes: { actionPanelButtonListType: React.PropTypes.func, - furtherDetailsType: React.PropTypes.func, - edition: React.PropTypes.object, coaError: React.PropTypes.object, currentUser: React.PropTypes.object, + edition: React.PropTypes.object, + furtherDetailsType: React.PropTypes.func, loadEdition: React.PropTypes.func }, @@ -50,6 +50,10 @@ let Edition = React.createClass({ }; }, + updateEdition() { + return EditionActions.fetchEdition(this.props.edition.bitcoin_id); + }, + render() { const { actionPanelButtonListType, @@ -64,7 +68,8 @@ let Edition = React.createClass({ + currentUser={currentUser} + refreshObject={this.updateEdition} />
    diff --git a/js/components/ascribe_detail/piece.js b/js/components/ascribe_detail/piece.js index d779b716..58648c7b 100644 --- a/js/components/ascribe_detail/piece.js +++ b/js/components/ascribe_detail/piece.js @@ -26,8 +26,7 @@ let Piece = React.createClass({ ]) }, - - updateObject() { + updatePiece() { return PieceActions.fetchOne(this.props.piece.id); }, @@ -40,7 +39,7 @@ let Piece = React.createClass({ + refreshObject={this.updatePiece} /> {header} From 1dca764166c207f2381c9f08c1962a38299e4c62 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Tue, 12 Jan 2016 12:34:55 +0100 Subject: [PATCH 26/94] Use Link instead of an anchor for AccordionList's empty placeholder to avoid page refresh --- js/components/ascribe_accordion_list/accordion_list.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/js/components/ascribe_accordion_list/accordion_list.js b/js/components/ascribe_accordion_list/accordion_list.js index 1046ab7f..f1967edb 100644 --- a/js/components/ascribe_accordion_list/accordion_list.js +++ b/js/components/ascribe_accordion_list/accordion_list.js @@ -1,6 +1,8 @@ 'use strict'; import React from 'react'; +import { Link } from 'react-router'; + import { getLangText } from '../../utils/lang_utils'; @@ -36,7 +38,7 @@ let AccordionList = React.createClass({

    {getLangText('To register one, click')}  - {getLangText('here')}! + {getLangText('here')}!

    ); From 315e5f01084dbd2644265f8bd0e9d84ec9c3d341 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Tue, 12 Jan 2016 15:05:00 +0100 Subject: [PATCH 27/94] Add `.isRequired` to App proptypes --- js/components/ascribe_app.js | 5 +++-- .../whitelabel/prize/portfolioreview/pr_app.js | 10 ++++++---- .../whitelabel/prize/simple_prize/prize_app.js | 10 ++++++---- js/components/whitelabel/wallet/wallet_app.js | 11 +++++++---- 4 files changed, 22 insertions(+), 14 deletions(-) diff --git a/js/components/ascribe_app.js b/js/components/ascribe_app.js index 369bf744..f7000aca 100644 --- a/js/components/ascribe_app.js +++ b/js/components/ascribe_app.js @@ -18,11 +18,12 @@ import { mergeOptions } from '../utils/general_utils'; let AscribeApp = React.createClass({ propTypes: { + routes: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, + children: React.PropTypes.oneOfType([ React.PropTypes.arrayOf(React.PropTypes.element), React.PropTypes.element - ]), - routes: React.PropTypes.arrayOf(React.PropTypes.object) + ]) }, getInitialState() { diff --git a/js/components/whitelabel/prize/portfolioreview/pr_app.js b/js/components/whitelabel/prize/portfolioreview/pr_app.js index 85091a8b..be0b0d8e 100644 --- a/js/components/whitelabel/prize/portfolioreview/pr_app.js +++ b/js/components/whitelabel/prize/portfolioreview/pr_app.js @@ -21,12 +21,13 @@ import { getCookie } from '../../../../utils/fetch_api_utils'; let PRApp = React.createClass({ propTypes: { + history: React.PropTypes.object.isRequired, + routes: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, + children: React.PropTypes.oneOfType([ React.PropTypes.arrayOf(React.PropTypes.element), React.PropTypes.element - ]), - history: React.PropTypes.object, - routes: React.PropTypes.arrayOf(React.PropTypes.object) + ]) }, getInitialState() { @@ -54,7 +55,8 @@ let PRApp = React.createClass({ }, render() { - const { children, currentUser, history, routes, whitelabel } = this.props; + const { children, history, routes } = this.props; + const { currentUser, whitelabel } = this.state; const subdomain = getSubdomain(); // Add the current user and whitelabel settings to all child routes diff --git a/js/components/whitelabel/prize/simple_prize/prize_app.js b/js/components/whitelabel/prize/simple_prize/prize_app.js index e9083428..9ca99ca3 100644 --- a/js/components/whitelabel/prize/simple_prize/prize_app.js +++ b/js/components/whitelabel/prize/simple_prize/prize_app.js @@ -20,12 +20,13 @@ import { getSubdomain, mergeOptions } from '../../../../utils/general_utils'; let PrizeApp = React.createClass({ propTypes: { + history: React.PropTypes.object.isRequired, + routes: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, + children: React.PropTypes.oneOfType([ React.PropTypes.arrayOf(React.PropTypes.element), React.PropTypes.element - ]), - history: React.PropTypes.object, - routes: React.PropTypes.arrayOf(React.PropTypes.object) + ]) }, getInitialState() { @@ -53,7 +54,8 @@ let PrizeApp = React.createClass({ }, render() { - const { children, currentUser, history, routes, whitelabel } = this.props; + const { children, history, routes } = this.props; + const { currentUser, whitelabel } = this.state; const subdomain = getSubdomain(); // The second element of routes is always the active component object, where we can diff --git a/js/components/whitelabel/wallet/wallet_app.js b/js/components/whitelabel/wallet/wallet_app.js index 783bb2ab..4508a278 100644 --- a/js/components/whitelabel/wallet/wallet_app.js +++ b/js/components/whitelabel/wallet/wallet_app.js @@ -19,12 +19,13 @@ import { getSubdomain, mergeOptions } from '../../../utils/general_utils'; let WalletApp = React.createClass({ propTypes: { + history: React.PropTypes.object.isRequired, + routes: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, + children: React.PropTypes.oneOfType([ React.PropTypes.arrayOf(React.PropTypes.element), React.PropTypes.element - ]), - history: React.PropTypes.object, - routes: React.PropTypes.arrayOf(React.PropTypes.object) + ]) }, getInitialState() { @@ -38,6 +39,7 @@ let WalletApp = React.createClass({ UserStore.listen(this.onChange); WhitelabelStore.listen(this.onChange); + console.log('fetch'); UserActions.fetchCurrentUser(); WhitelabelActions.fetchWhitelabel(); }, @@ -52,7 +54,8 @@ let WalletApp = React.createClass({ }, render() { - const { children, currentUser, history, routes, whitelabel } = this.props; + const { children, history, routes } = this.props; + const { currentUser, whitelabel } = this.state; const subdomain = getSubdomain(); // The second element of routes is always the active component object, where we can From 5b2178eb7ea3be8e67b8fb42be2cc9a785ef15c1 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Tue, 12 Jan 2016 15:06:43 +0100 Subject: [PATCH 28/94] Use height for landing page logos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Using height means the browser can allocate space for the image before it loads, so there’s no flash of movement --- .../whitelabel/wallet/components/23vivi/23vivi_landing.js | 5 ++++- .../whitelabel/wallet/components/cyland/cyland_landing.js | 2 +- .../whitelabel/wallet/components/lumenus/lumenus_landing.js | 2 +- sass/whitelabel/wallet/23vivi/23vivi_custom_style.scss | 1 - 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/js/components/whitelabel/wallet/components/23vivi/23vivi_landing.js b/js/components/whitelabel/wallet/components/23vivi/23vivi_landing.js index 1219c182..386f2145 100644 --- a/js/components/whitelabel/wallet/components/23vivi/23vivi_landing.js +++ b/js/components/whitelabel/wallet/components/23vivi/23vivi_landing.js @@ -30,7 +30,10 @@ let Vivi23Landing = React.createClass({
    - +
    {getLangText('Artwork from the 23VIVI Marketplace is powered by') + ' '} diff --git a/js/components/whitelabel/wallet/components/cyland/cyland_landing.js b/js/components/whitelabel/wallet/components/cyland/cyland_landing.js index deb0bd71..7919fec4 100644 --- a/js/components/whitelabel/wallet/components/cyland/cyland_landing.js +++ b/js/components/whitelabel/wallet/components/cyland/cyland_landing.js @@ -30,7 +30,7 @@ let CylandLanding = React.createClass({
    - +
    {getLangText('Submissions to Cyland Archive are powered by') + ' '} diff --git a/js/components/whitelabel/wallet/components/lumenus/lumenus_landing.js b/js/components/whitelabel/wallet/components/lumenus/lumenus_landing.js index 5f5020a3..9674ef3f 100644 --- a/js/components/whitelabel/wallet/components/lumenus/lumenus_landing.js +++ b/js/components/whitelabel/wallet/components/lumenus/lumenus_landing.js @@ -29,7 +29,7 @@ let LumenusLanding = React.createClass({
    - +
    {getLangText('Artwork from the Lumenus Marketplace is powered by') + ' '} diff --git a/sass/whitelabel/wallet/23vivi/23vivi_custom_style.scss b/sass/whitelabel/wallet/23vivi/23vivi_custom_style.scss index a5026272..348564bc 100644 --- a/sass/whitelabel/wallet/23vivi/23vivi_custom_style.scss +++ b/sass/whitelabel/wallet/23vivi/23vivi_custom_style.scss @@ -34,7 +34,6 @@ $vivi23--highlight-color: #de2600; .vivi23-landing--header-logo { margin-top: 1em; margin-bottom: 2em; - height: 75px; } } From b22ae7def1ae9804193c4dc59e46bb44b7dcb65a Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Tue, 12 Jan 2016 15:07:38 +0100 Subject: [PATCH 29/94] Small bug fixes for components affected by the UserStore and WhitelabelStore being passed down as a prop --- js/components/ascribe_detail/edition.js | 96 +++++++++---------- .../ascribe_detail/edition_action_panel.js | 22 +++-- .../ascribe_detail/edition_container.js | 8 +- js/components/ascribe_forms/form_signup.js | 2 +- .../ikonotv/ikonotv_register_piece.js | 16 ++-- .../market_buttons/market_acl_button_list.js | 5 +- .../market_additional_data_form.js | 5 +- 7 files changed, 77 insertions(+), 77 deletions(-) diff --git a/js/components/ascribe_detail/edition.js b/js/components/ascribe_detail/edition.js index 46eb8183..deb275ec 100644 --- a/js/components/ascribe_detail/edition.js +++ b/js/components/ascribe_detail/edition.js @@ -36,10 +36,12 @@ import { getLangText } from '../../utils/lang_utils'; */ let Edition = React.createClass({ propTypes: { + currentUser: React.PropTypes.object.isRequired, + edition: React.PropTypes.object.isRequired, + whitelabel: React.PropTypes.object.isRequired, + actionPanelButtonListType: React.PropTypes.func, coaError: React.PropTypes.object, - currentUser: React.PropTypes.object, - edition: React.PropTypes.object, furtherDetailsType: React.PropTypes.func, loadEdition: React.PropTypes.func }, @@ -50,10 +52,6 @@ let Edition = React.createClass({ }; }, - updateEdition() { - return EditionActions.fetchEdition(this.props.edition.bitcoin_id); - }, - render() { const { actionPanelButtonListType, @@ -61,7 +59,8 @@ let Edition = React.createClass({ currentUser, edition, furtherDetailsType: FurtherDetailsType, - loadEdition } = this.props; + loadEdition, + whitelabel } = this.props; return ( @@ -69,49 +68,47 @@ let Edition = React.createClass({ + refreshObject={loadEdition} />
    -
    +

    {edition.title}

    -
    +
    + handleSuccess={loadEdition} + whitelabel={whitelabel} /> + editionId={edition.bitcoin_id} /> 0}> - + show={edition.ownership_history && edition.ownership_history.length}> + 0}> - + 0}> - + + currentUser={currentUser} /> {return {'bitcoin_id': edition.bitcoin_id}; }} label={getLangText('Personal note (public)')} @@ -135,13 +132,11 @@ let Edition = React.createClass({ show={!!edition.public_note || !!edition.acl.acl_edit} successMessage={getLangText('Public edition note saved')} url={ApiUrls.note_public_edition} - currentUser={currentUser}/> + currentUser={currentUser} /> 0 || - edition.other_data.length > 0}> + show={edition.acl.acl_edit || Object.keys(edition.extra_data).length || edition.other_data.length}> - - + +
    @@ -163,60 +156,56 @@ let Edition = React.createClass({ let EditionSummary = React.createClass({ propTypes: { + currentUser: React.PropTypes.object.isRequired, + edition: React.PropTypes.object.isRequired, + whitelabel: React.PropTypes.object.isRequired, + actionPanelButtonListType: React.PropTypes.func, - edition: React.PropTypes.object, - currentUser: React.PropTypes.object, handleSuccess: React.PropTypes.func }, - handleSuccess() { - this.props.handleSuccess(); - }, + getStatus() { + const { status } = this.props.edition; - getStatus(){ - let status = null; - if (this.props.edition.status.length > 0){ - let statusStr = this.props.edition.status.join(', ').replace(/_/g, ' '); - status = ; - if (this.props.edition.pending_new_owner && this.props.edition.acl.acl_withdraw_transfer){ - status = ( - - ); - } - } - return status; + return status.length ? ( + + ) : null; }, render() { - let { actionPanelButtonListType, edition, currentUser } = this.props; + const { actionPanelButtonListType, currentUser, edition, handleSuccess, whitelabel } = this.props; + return (
    + value={edition.edition_number + ' ' + getLangText('of') + ' ' + edition.num_editions} /> - + value={edition.owner} /> + {this.getStatus()} {/* `acl_view` is always available in `edition.acl`, therefore if it has no more than 1 key, we're hiding the `DetailProperty` actions as otherwise `AclInformation` would show up */} - 1}> + 1}> + edition={edition} + handleSuccess={handleSuccess} + whitelabel={whitelabel} />
    @@ -365,4 +354,5 @@ let SpoolDetails = React.createClass({ } }); + export default Edition; diff --git a/js/components/ascribe_detail/edition_action_panel.js b/js/components/ascribe_detail/edition_action_panel.js index d53617bc..2d9c9a26 100644 --- a/js/components/ascribe_detail/edition_action_panel.js +++ b/js/components/ascribe_detail/edition_action_panel.js @@ -36,8 +36,9 @@ import { getLangText } from '../../utils/lang_utils'; */ let EditionActionPanel = React.createClass({ propTypes: { - edition: React.PropTypes.object.isRequired, currentUser: React.PropTypes.object.isRequired, + edition: React.PropTypes.object.isRequired, + whitelabel: React.PropTypes.object.isRequired, actionPanelButtonListType: React.PropTypes.func, handleSuccess: React.PropTypes.func @@ -87,21 +88,25 @@ let EditionActionPanel = React.createClass({ handleSuccess(response) { this.refreshCollection(); - this.props.handleSuccess(); - if (response){ - let notification = new GlobalNotificationModel(response.notification, 'success'); + + if (response) { + const notification = new GlobalNotificationModel(response.notification, 'success'); GlobalNotificationActions.appendGlobalNotification(notification); } + + if (typeof this.props.handleSuccess === 'function') { + this.props.handleSuccess(); + } }, render() { const { actionPanelButtonListType: ActionPanelButtonListType, + currentUser, edition, - currentUser } = this.props; + whitelabel } = this.props; - if (edition.notifications && - edition.notifications.length > 0){ + if (edition.notifications && edition.notifications.length) { return ( + whitelabel={whitelabel}> diff --git a/js/components/ascribe_detail/edition_container.js b/js/components/ascribe_detail/edition_container.js index 1001be55..beb2e9cb 100644 --- a/js/components/ascribe_detail/edition_container.js +++ b/js/components/ascribe_detail/edition_container.js @@ -74,11 +74,11 @@ let EditionContainer = React.createClass({ onChange(state) { this.setState(state); - if(state && state.edition && state.edition.digital_work) { - let isEncoding = state.edition.digital_work.isEncoding; + if (state && state.edition && state.edition.digital_work) { + const isEncoding = state.edition.digital_work.isEncoding; if (state.edition.digital_work.mime === 'video' && typeof isEncoding === 'number' && isEncoding !== 100 && !this.state.timerId) { - let timerId = window.setInterval(() => EditionActions.fetchOne(this.props.params.editionId), 10000); - this.setState({timerId: timerId}); + const timerId = window.setInterval(() => EditionActions.fetchOne(this.props.params.editionId), 10000); + this.setState({ timerId: timerId }); } } }, diff --git a/js/components/ascribe_forms/form_signup.js b/js/components/ascribe_forms/form_signup.js index 69d50005..6faa6cf8 100644 --- a/js/components/ascribe_forms/form_signup.js +++ b/js/components/ascribe_forms/form_signup.js @@ -95,7 +95,7 @@ let SignupForm = React.createClass({ type="email" placeholder={getLangText('(e.g. andy@warhol.co.uk)')} autoComplete="on" - defaultValue={email} + defaultValue={emailQuery} required/> diff --git a/js/components/whitelabel/wallet/components/market/market_forms/market_additional_data_form.js b/js/components/whitelabel/wallet/components/market/market_forms/market_additional_data_form.js index d136c9cf..0698088f 100644 --- a/js/components/whitelabel/wallet/components/market/market_forms/market_additional_data_form.js +++ b/js/components/whitelabel/wallet/components/market/market_forms/market_additional_data_form.js @@ -115,7 +115,7 @@ let MarketAdditionalDataForm = React.createClass({ this.props.handleSuccess(); } - let notification = new GlobalNotificationModel(getLangText('Further details successfully updated'), 'success', 10000); + const notification = new GlobalNotificationModel(getLangText('Further details successfully updated'), 'success', 10000); GlobalNotificationActions.appendGlobalNotification(notification); }, @@ -128,7 +128,8 @@ let MarketAdditionalDataForm = React.createClass({ render() { const { editable, isInline, handleSuccess, showHeading, showNotification, submitLabel } = this.props; const { piece } = this.state; - let buttons, heading; + let buttons; + let heading; let spinner = ; From 1ffa1eb6aa7ecdfbe657ed04a1e8a19fe8db5fcc Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Wed, 13 Jan 2016 16:03:08 +0100 Subject: [PATCH 30/94] Fix potential race condition causing ProxyHandler to not evaluate its redirect functions if the component doesn't get updated --- js/components/ascribe_routes/proxy_handler.js | 26 ++++++++++++++----- js/stores/user_store.js | 16 +++++++++++- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/js/components/ascribe_routes/proxy_handler.js b/js/components/ascribe_routes/proxy_handler.js index 55987ee5..225420b0 100644 --- a/js/components/ascribe_routes/proxy_handler.js +++ b/js/components/ascribe_routes/proxy_handler.js @@ -34,8 +34,8 @@ export function AuthRedirect({to, when}) { // // So if when === 'loggedIn', we're checking if the user is logged in (and // vice versa) - const exprToValidate = when === 'loggedIn' ? currentUser && currentUser.email - : currentUser && !currentUser.email; + const isLoggedIn = currentUser && Object.keys(currentUser).length && currentUser.email; + const exprToValidate = when === 'loggedIn' ? isLoggedIn : !isLoggedIn; // and redirect if `true`. if (exprToValidate) { @@ -80,7 +80,11 @@ export function ProxyHandler(...redirectFunctions) { displayName: 'ProxyHandler', propTypes: { - currentUser: object, + // Provided from AscribeApp + currentUser: React.PropTypes.object, + whitelabel: React.PropTypes.object, + + // Provided from router location: object }, @@ -88,10 +92,18 @@ export function ProxyHandler(...redirectFunctions) { // to use the `Lifecycle` widget in further down nested components mixins: [RouteContext], - componentDidUpdate() { - const { currentUser, location: { query } } = this.props; + componentDidMount() { + this.evaluateRedirectFunctions(); + }, - if (!UserStore.isLoading()) { + componentWillReceiveProps(nextProps) { + this.evaluateRedirectFunctions(nextProps); + }, + + evaluateRedirectFunctions(props = this.props) { + const { currentUser, location: { query } } = props; + + if (UserStore.hasLoaded() && !UserStore.isLoading()) { for (let i = 0; i < redirectFunctions.length; i++) { // if a redirectFunction redirects the user, // it should return `true` and therefore @@ -106,7 +118,7 @@ export function ProxyHandler(...redirectFunctions) { render() { return ( - + ); } }); diff --git a/js/stores/user_store.js b/js/stores/user_store.js index f07e56e7..41a9c962 100644 --- a/js/stores/user_store.js +++ b/js/stores/user_store.js @@ -12,12 +12,16 @@ class UserStore { constructor() { this.currentUser = {}; this.userMeta = { + hasLoaded: false, invalidateCache: false, err: null }; this.bindActions(UserActions); this.registerAsync(UserSource); + this.exportPublicMethods({ + hasLoaded: this.hasLoaded.bind(this) + }); } onFetchCurrentUser(invalidateCache) { @@ -28,7 +32,8 @@ class UserStore { } } - onSuccessFetchCurrentUser({users: [user = {}]}) { + onSuccessFetchCurrentUser({ users: [ user = {} ] = [] }) { + this.userMeta.hasLoaded = true; this.userMeta.invalidateCache = false; this.userMeta.err = null; @@ -50,6 +55,10 @@ class UserStore { altWhitelabel.recycle(); altUser.recycle(); altThirdParty.recycle(); + + // Since we've just logged out, we can set this store's + // hasLoaded flag back to true as there is no current user. + this.userMeta.hasLoaded = true; }); } @@ -59,8 +68,13 @@ class UserStore { onErrorCurrentUser(err) { console.logGlobal(err); + this.userMeta.hasLoaded = true; this.userMeta.err = err; } + + hasLoaded() { + return this.userMeta.hasLoaded; + } } export default altUser.createStore(UserStore, 'UserStore'); From 2ad2ea242d068cc7254c3d535390bcd033a10fb9 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Mon, 18 Jan 2016 17:31:56 +0100 Subject: [PATCH 31/94] Changes for PR comments --- .../accordion_list_item_wallet.js | 2 +- .../ascribe_buttons/acls/acl_button.js | 5 ++- .../ascribe_detail/media_container.js | 5 ++- js/components/ascribe_forms/form_login.js | 2 +- js/components/password_reset_container.js | 37 +++++++------------ .../prize/portfolioreview/pr_app.js | 8 ---- .../components/prize_register_piece.js | 4 +- .../sluice_selected_prize_action_button.js | 2 +- .../cyland/cyland_register_piece.js | 9 ++--- .../ikonotv/ikonotv_contract_notifications.js | 5 ++- .../ikonotv/ikonotv_register_piece.js | 13 +++---- .../components/market/market_piece_list.js | 2 - js/components/whitelabel/wallet/wallet_app.js | 1 - js/utils/general_utils.js | 2 +- 14 files changed, 37 insertions(+), 60 deletions(-) diff --git a/js/components/ascribe_accordion_list/accordion_list_item_wallet.js b/js/components/ascribe_accordion_list/accordion_list_item_wallet.js index 6f3139a3..a3266be9 100644 --- a/js/components/ascribe_accordion_list/accordion_list_item_wallet.js +++ b/js/components/ascribe_accordion_list/accordion_list_item_wallet.js @@ -90,7 +90,7 @@ let AccordionListItemWallet = React.createClass({ this.state.orderBy, this.state.orderAsc, this.state.filterBy); EditionListActions.toggleEditionList(pieceId); - const notification = new GlobalNotificationModel('Editions successfully created', 'success', 10000); + const notification = new GlobalNotificationModel(getLangText('Editions successfully created'), 'success', 10000); GlobalNotificationActions.appendGlobalNotification(notification); }, diff --git a/js/components/ascribe_buttons/acls/acl_button.js b/js/components/ascribe_buttons/acls/acl_button.js index 8cfdddce..2dc21d31 100644 --- a/js/components/ascribe_buttons/acls/acl_button.js +++ b/js/components/ascribe_buttons/acls/acl_button.js @@ -24,6 +24,8 @@ export default function ({ action, displayName, title, tooltip }) { propTypes: { availableAcls: React.PropTypes.object.isRequired, + handleSuccess: React.PropTypes.func.isRequired, + buttonAcceptName: React.PropTypes.string, buttonAcceptClassName: React.PropTypes.string, currentUser: React.PropTypes.object, @@ -32,14 +34,13 @@ export default function ({ action, displayName, title, tooltip }) { React.PropTypes.object, React.PropTypes.array ]).isRequired, - handleSuccess: React.PropTypes.func.isRequired, className: React.PropTypes.string }, sanitizeAction() { const { buttonAcceptName } = this.props; - return buttonAcceptName ? buttonAcceptName : AclInformationText.titles[action]; + return buttonAcceptName || AclInformationText.titles[action]; }, render() { diff --git a/js/components/ascribe_detail/media_container.js b/js/components/ascribe_detail/media_container.js index ebc5f1d0..8ea374b3 100644 --- a/js/components/ascribe_detail/media_container.js +++ b/js/components/ascribe_detail/media_container.js @@ -43,8 +43,9 @@ let MediaContainer = React.createClass({ const isEncoding = digitalWork.isEncoding; if (digitalWork.mime === 'video' && typeof isEncoding === 'number' && isEncoding !== 100 && !this.state.timerId) { - const timerId = window.setInterval(refreshObject, ENCODE_UPDATE_TIME); - this.setState({ timerId: timerId }); + this.setState({ + timerId: window.setInterval(refreshObject, ENCODE_UPDATE_TIME) + }); } } }, diff --git a/js/components/ascribe_forms/form_login.js b/js/components/ascribe_forms/form_login.js index 86d94456..67069cac 100644 --- a/js/components/ascribe_forms/form_login.js +++ b/js/components/ascribe_forms/form_login.js @@ -35,7 +35,7 @@ let LoginForm = React.createClass({ }, handleSuccess({ success }) { - const notification = new GlobalNotificationModel('Login successful', 'success', 10000); + const notification = new GlobalNotificationModel(getLangText('Login successful'), 'success', 10000); GlobalNotificationActions.appendGlobalNotification(notification); if (success) { diff --git a/js/components/password_reset_container.js b/js/components/password_reset_container.js index 1649479e..773e1a63 100644 --- a/js/components/password_reset_container.js +++ b/js/components/password_reset_container.js @@ -29,7 +29,7 @@ let PasswordResetContainer = React.createClass({ }, handleRequestSuccess(email) { - this.setState({ isRequested: email }); + this.setState({ isRequested: !!email }); }, render() { @@ -38,31 +38,20 @@ let PasswordResetContainer = React.createClass({ if (emailQuery && tokenQuery) { return ( -
    - -
    + + ); + } else if (!isRequested) { + return ( + ); } else { - if (isRequested === false) { - return ( -
    - -
    - ); - } else if (isRequested) { - return ( -
    -
    - {getLangText('If your email address exists in our database, you will receive a password recovery link in a few minutes.')} -
    -
    - ); - } else { - return ; - } + return ( +
    + {getLangText('If your email address exists in our database, you will receive a password recovery link in a few minutes.')} +
    + ); } } }); diff --git a/js/components/whitelabel/prize/portfolioreview/pr_app.js b/js/components/whitelabel/prize/portfolioreview/pr_app.js index be0b0d8e..a34e7968 100644 --- a/js/components/whitelabel/prize/portfolioreview/pr_app.js +++ b/js/components/whitelabel/prize/portfolioreview/pr_app.js @@ -59,14 +59,6 @@ let PRApp = React.createClass({ const { currentUser, whitelabel } = this.state; const subdomain = getSubdomain(); - // Add the current user and whitelabel settings to all child routes - const childrenWithProps = React.Children.map(children, (child) => { - return React.cloneElement(child, { - currentUser, - whitelabel - }); - }); - let style = {}; let header; if (currentUser && currentUser.email && history.isActive(`/pieces/${getCookie(currentUser.email)}`)) { diff --git a/js/components/whitelabel/prize/simple_prize/components/prize_register_piece.js b/js/components/whitelabel/prize/simple_prize/components/prize_register_piece.js index e316bfac..fcb304ca 100644 --- a/js/components/whitelabel/prize/simple_prize/components/prize_register_piece.js +++ b/js/components/whitelabel/prize/simple_prize/components/prize_register_piece.js @@ -42,7 +42,6 @@ let PrizeRegisterPiece = React.createClass({ }, render() { - const { location } = this.props; const { prize } = this.state; setDocumentTitle(getLangText('Submit to the prize')); @@ -53,8 +52,7 @@ let PrizeRegisterPiece = React.createClass({ {...this.props} enableLocalHashing={false} headerMessage={''} - submitMessage={getLangText('Submit')} - location={location}> + submitMessage={getLangText('Submit')}> 0} enableLocalHashing={false} - headerMessage={getLangText('Submit to Cyland Archive')} - submitMessage={getLangText('Submit')} - isFineUploaderActive={true} handleSuccess={this.handleRegisterSuccess} - location={location} /> + headerMessage={getLangText('Submit to Cyland Archive')} + isFineUploaderActive={true} + submitMessage={getLangText('Submit')} />
    diff --git a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_contract_notifications.js b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_contract_notifications.js index b2921179..60e5f10d 100644 --- a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_contract_notifications.js +++ b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_contract_notifications.js @@ -177,7 +177,7 @@ let IkonotvContractNotifications = React.createClass({ - Download PDF version + {getLangText('Download PDF version')}
    @@ -194,8 +194,9 @@ let IkonotvContractNotifications = React.createClass({
    ); + } else { + return null; } - return null; } }); diff --git a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_register_piece.js b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_register_piece.js index a0e4f67f..466aa9fe 100644 --- a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_register_piece.js +++ b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_register_piece.js @@ -191,7 +191,7 @@ let IkonotvRegisterPiece = React.createClass({ const { whitelabel } = this.props; const { piece } = this.state; const today = new Moment(); - const endDate = (new Moment()).add(2, 'years'); + const endDate = new Moment().add(2, 'years'); return (
    @@ -219,7 +219,7 @@ let IkonotvRegisterPiece = React.createClass({ }, render() { - const { currentUser, location } = this.props; + const { location } = this.props; const { pageExitWarning, step } = this.state; return ( @@ -236,14 +236,13 @@ let IkonotvRegisterPiece = React.createClass({ 0} enableLocalHashing={false} - headerMessage={getLangText('Register work')} - submitMessage={getLangText('Register')} - isFineUploaderActive={true} handleSuccess={this.handleRegisterSuccess} - location={location} /> + headerMessage={getLangText('Register work')} + isFineUploaderActive={true} + submitMessage={getLangText('Register')} />
    diff --git a/js/components/whitelabel/wallet/components/market/market_piece_list.js b/js/components/whitelabel/wallet/components/market/market_piece_list.js index 404926f3..4fb89e7c 100644 --- a/js/components/whitelabel/wallet/components/market/market_piece_list.js +++ b/js/components/whitelabel/wallet/components/market/market_piece_list.js @@ -29,7 +29,6 @@ let MarketPieceList = React.createClass({ render() { const { currentUser: { email: userEmail }, - customThumbnailPlaceholder, whitelabel: { name: whitelabelName = 'Market', user: whitelabelAdminEmail @@ -58,7 +57,6 @@ let MarketPieceList = React.createClass({ canLoadPieceList={canLoadPieceList} redirectTo="/register_piece?slide_num=0" bulkModalButtonListType={MarketAclButtonList} - customThumbnailPlaceholder={customThumbnailPlaceholder} filterParams={filterParams} /> ); } diff --git a/js/components/whitelabel/wallet/wallet_app.js b/js/components/whitelabel/wallet/wallet_app.js index 4508a278..c5b3d062 100644 --- a/js/components/whitelabel/wallet/wallet_app.js +++ b/js/components/whitelabel/wallet/wallet_app.js @@ -39,7 +39,6 @@ let WalletApp = React.createClass({ UserStore.listen(this.onChange); WhitelabelStore.listen(this.onChange); - console.log('fetch'); UserActions.fetchCurrentUser(); WhitelabelActions.fetchWhitelabel(); }, diff --git a/js/utils/general_utils.js b/js/utils/general_utils.js index a3336d80..8980fd93 100644 --- a/js/utils/general_utils.js +++ b/js/utils/general_utils.js @@ -103,7 +103,7 @@ function _doesObjectListHaveDuplicates(l) { // Taken from: http://stackoverflow.com/a/7376645/1263876 // By casting the array to a set, and then checking if the size of the array // shrunk in the process of casting, we can check if there were any duplicates - return (new Set(mergedList)).size !== mergedList.length; + return new Set(mergedList).size !== mergedList.length; } /** From b24e66ed112b77676503f0412e90c7ed8d0b6608 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Mon, 18 Jan 2016 19:14:34 +0100 Subject: [PATCH 32/94] Fix logout not working properly after moving logic to UserStore --- js/stores/user_store.js | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/js/stores/user_store.js b/js/stores/user_store.js index 41a9c962..6289af5e 100644 --- a/js/stores/user_store.js +++ b/js/stores/user_store.js @@ -45,25 +45,21 @@ class UserStore { } onLogoutCurrentUser() { - this.getInstance() - .performLogoutCurrentUser() - .then(() => { - EventActions.userDidLogout(); - - // Reset all stores back to their initial state - alt.recycle(); - altWhitelabel.recycle(); - altUser.recycle(); - altThirdParty.recycle(); - - // Since we've just logged out, we can set this store's - // hasLoaded flag back to true as there is no current user. - this.userMeta.hasLoaded = true; - }); + this.getInstance().performLogoutCurrentUser(); } onSuccessLogoutCurrentUser() { - this.currentUser = {}; + EventActions.userDidLogout(); + + // Reset all stores back to their initial state + alt.recycle(); + altWhitelabel.recycle(); + altUser.recycle(); + altThirdParty.recycle(); + + // Since we've just logged out, we can set this store's + // hasLoaded flag back to true as there is no current user. + this.userMeta.hasLoaded = true; } onErrorCurrentUser(err) { From 7e1cfbb4901b0a7337e9d7deedc2df3848239fcd Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Tue, 19 Jan 2016 15:00:50 +0100 Subject: [PATCH 33/94] Add AppBase HOC to improve DRYness of functionality reused across apps --- js/components/app_base.js | 55 +++++++++++++++++++ js/components/ascribe_app.js | 31 ++--------- .../ascribe_buttons/acls/acl_button.js | 2 +- .../prize/portfolioreview/pr_app.js | 34 ++++++------ .../prize/simple_prize/prize_app.js | 28 +++++----- js/components/whitelabel/wallet/wallet_app.js | 30 +++++----- 6 files changed, 105 insertions(+), 75 deletions(-) create mode 100644 js/components/app_base.js diff --git a/js/components/app_base.js b/js/components/app_base.js new file mode 100644 index 00000000..044b984c --- /dev/null +++ b/js/components/app_base.js @@ -0,0 +1,55 @@ +'use strict'; + +import React from 'react'; +import classNames from 'classnames'; +import { History } from 'react-router'; + +import Footer from './footer'; +import GlobalNotification from './global_notification'; + +import AppConstants from '../constants/application_constants'; + + +export default function AppBase(App) { + return React.createClass({ + displayName: 'AppBase', + + propTypes: { + history: React.PropTypes.object.isRequired, + location: React.PropTypes.object.isRequired, + routes: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, + + children: React.PropTypes.oneOfType([ + React.PropTypes.arrayOf(React.PropTypes.element), + React.PropTypes.element + ]) + }, + + mixins: [History], + + componentDidMount() { + this.history.locationQueue.push(this.props.location); + }, + + componentWillReceiveProps(nextProps) { + const { locationQueue } = this.history; + locationQueue.unshift(nextProps.location); + + // Limit the number of locations to keep in memory to avoid too much memory usage + if (locationQueue.length > AppConstants.locationThreshold) { + locationQueue.length = AppConstants.locationThreshold; + } + }, + + render() { + return ( +
    + +
    + + + ); + } + }); +}; diff --git a/js/components/ascribe_app.js b/js/components/ascribe_app.js index 774395ae..6d159c26 100644 --- a/js/components/ascribe_app.js +++ b/js/components/ascribe_app.js @@ -1,39 +1,21 @@ 'use strict'; import React from 'react'; -import { History } from 'react-router'; +import AppBase from './app_base'; import Header from './header'; -import Footer from './footer'; -import GlobalNotification from './global_notification'; import AppConstants from '../constants/application_constants'; let AscribeApp = React.createClass({ propTypes: { + routes: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, + children: React.PropTypes.oneOfType([ React.PropTypes.arrayOf(React.PropTypes.element), React.PropTypes.element - ]), - routes: React.PropTypes.arrayOf(React.PropTypes.object), - location: React.PropTypes.object - }, - - mixins: [History], - - componentDidMount() { - this.history.locationQueue.push(this.props.location); - }, - - componentWillReceiveProps(nextProps) { - const { locationQueue } = this.history; - locationQueue.unshift(nextProps.location); - - // Limit the number of locations to keep in memory to avoid too much memory usage - if (locationQueue.length > AppConstants.locationThreshold) { - locationQueue.length = AppConstants.locationThreshold; - } + ]) }, render() { @@ -46,12 +28,9 @@ let AscribeApp = React.createClass({
    {children}
    -
    - -
    ); } }); -export default AscribeApp; +export default AppBase(AscribeApp); diff --git a/js/components/ascribe_buttons/acls/acl_button.js b/js/components/ascribe_buttons/acls/acl_button.js index 97f2e173..2525c52a 100644 --- a/js/components/ascribe_buttons/acls/acl_button.js +++ b/js/components/ascribe_buttons/acls/acl_button.js @@ -14,7 +14,7 @@ import AppConstants from '../../../constants/application_constants'; import { AclInformationText } from '../../../constants/acl_information_text'; -export default function ({ action, displayName, title, tooltip }) { +export default function AclButton({ action, displayName, title, tooltip }) { if (AppConstants.aclList.indexOf(action) < 0) { console.warn('Your specified aclName did not match a an acl class.'); } diff --git a/js/components/whitelabel/prize/portfolioreview/pr_app.js b/js/components/whitelabel/prize/portfolioreview/pr_app.js index c672bdf5..2fe1c2d6 100644 --- a/js/components/whitelabel/prize/portfolioreview/pr_app.js +++ b/js/components/whitelabel/prize/portfolioreview/pr_app.js @@ -1,28 +1,30 @@ 'use strict'; import React from 'react'; -import GlobalNotification from '../../../global_notification'; - -import Hero from './components/pr_hero'; -import Header from '../../../header'; import EventActions from '../../../../actions/event_actions'; import UserStore from '../../../../stores/user_store'; import UserActions from '../../../../actions/user_actions'; +import Hero from './components/pr_hero'; + +import AppBase from '../../../app_base'; +import Header from '../../../header'; + import { getSubdomain } from '../../../../utils/general_utils'; import { getCookie } from '../../../../utils/fetch_api_utils'; let PRApp = React.createClass({ propTypes: { + history: React.PropTypes.object.isRequired, + routes: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, + children: React.PropTypes.oneOfType([ React.PropTypes.arrayOf(React.PropTypes.element), React.PropTypes.element - ]), - history: React.PropTypes.object, - routes: React.PropTypes.arrayOf(React.PropTypes.object) + ]) }, getInitialState() { @@ -55,19 +57,19 @@ let PRApp = React.createClass({ this.setState(state); }, + render() { - const { history, children, routes } = this.props; + const { children, history, routes } = this.props; const { currentUser } = this.state; + const subdomain = getSubdomain(); + let style = {}; - let subdomain = getSubdomain(); let header; - - if (currentUser && currentUser.email && history.isActive(`/pieces/${getCookie(currentUser.email)}`)) { - header = ; + header = (); style = { paddingTop: '0 !important' }; - } else if(currentUser && (currentUser.is_admin || currentUser.is_jury || currentUser.is_judge)) { - header =
    ; + } else if (currentUser && (currentUser.is_admin || currentUser.is_jury || currentUser.is_judge)) { + header = (
    ); } else { style = { paddingTop: '0 !important' }; } @@ -79,12 +81,10 @@ let PRApp = React.createClass({ style={style} className={'container ascribe-prize-app client--' + subdomain}> {children} - -
    ); } }); -export default PRApp; +export default AppBase(PRApp); diff --git a/js/components/whitelabel/prize/simple_prize/prize_app.js b/js/components/whitelabel/prize/simple_prize/prize_app.js index d5b55d5f..f1779437 100644 --- a/js/components/whitelabel/prize/simple_prize/prize_app.js +++ b/js/components/whitelabel/prize/simple_prize/prize_app.js @@ -1,50 +1,50 @@ 'use strict'; import React from 'react'; + import Hero from './components/prize_hero'; + +import AppBase from '../../../app_base'; import Header from '../../../header'; -import Footer from '../../../footer'; -import GlobalNotification from '../../../global_notification'; import { getSubdomain } from '../../../../utils/general_utils'; let PrizeApp = React.createClass({ propTypes: { + history: React.PropTypes.object.isRequired, + routes: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, + children: React.PropTypes.oneOfType([ React.PropTypes.arrayOf(React.PropTypes.element), React.PropTypes.element - ]), - history: React.PropTypes.object, - routes: React.PropTypes.arrayOf(React.PropTypes.object) + ]) }, render() { - const { history, routes } = this.props; - let header = null; - let subdomain = getSubdomain(); + const { children, history, routes } = this.props; + const subdomain = getSubdomain(); // The second element of routes is always the active component object, where we can // extract the path. let path = routes[1] ? routes[1].path : null; + let header = null; // if the path of the current activeRoute is not defined, then this is the IndexRoute if (!path || history.isActive('/login') || history.isActive('/signup')) { - header = ; + header = (); } else { - header =
    ; + header = (
    ); } return (
    {header} {this.props.children} - - -
    + {children}
    ); } }); -export default PrizeApp; +export default AppBase(PrizeApp); diff --git a/js/components/whitelabel/wallet/wallet_app.js b/js/components/whitelabel/wallet/wallet_app.js index bce7106b..57088969 100644 --- a/js/components/whitelabel/wallet/wallet_app.js +++ b/js/components/whitelabel/wallet/wallet_app.js @@ -1,41 +1,40 @@ 'use strict'; import React from 'react'; -import Header from '../../header'; -import Footer from '../../footer'; - -import GlobalNotification from '../../global_notification'; - import classNames from 'classnames'; +import AppBase from '../../app_base'; +import Header from '../../header'; + import { getSubdomain } from '../../../utils/general_utils'; let WalletApp = React.createClass({ propTypes: { + history: React.PropTypes.object.isRequired, + routes: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, + children: React.PropTypes.oneOfType([ React.PropTypes.arrayOf(React.PropTypes.element), React.PropTypes.element - ]), - history: React.PropTypes.object, - routes: React.PropTypes.arrayOf(React.PropTypes.object) + ]) }, render() { - let header = null; - let subdomain = getSubdomain(); - const { history, routes, children } = this.props; + const { children, history, routes } = this.props; + const subdomain = getSubdomain(); // The second element of routes is always the active component object, where we can // extract the path. let path = routes[1] ? routes[1].path : null; + let header = null; // if the path of the current activeRoute is not defined, then this is the IndexRoute if ((!path || history.isActive('/login') || history.isActive('/signup') || history.isActive('/contract_notifications')) && (['cyland', 'ikonotv', 'lumenus', '23vivi']).indexOf(subdomain) > -1) { - header = (
    ); + header = (
    ); } else { - header =
    ; + header = (
    ); } // In react-router 1.0, Routes have no 'name' property anymore. To keep functionality however, @@ -45,13 +44,10 @@ let WalletApp = React.createClass({
    {header} {children} - - -
    ); } }); -export default WalletApp; +export default AppBase(WalletApp); From efbc605fc88df0fc7f7f8cc2160b07e54219a863 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Tue, 19 Jan 2016 15:04:46 +0100 Subject: [PATCH 34/94] Standardize render structure across main app and white label apps Also fixes some whitelabel landing pages broken from the standardization and removes unnecessary class names --- js/components/ascribe_app.js | 6 +- .../prize/portfolioreview/pr_app.js | 9 +-- .../prize/simple_prize/prize_app.js | 6 +- .../components/23vivi/23vivi_landing.js | 2 +- .../components/cyland/cyland_landing.js | 64 ++++++++++--------- js/components/whitelabel/wallet/wallet_app.js | 7 +- .../wallet/23vivi/23vivi_custom_style.scss | 5 ++ .../wallet/cyland/cyland_custom_style.scss | 5 ++ 8 files changed, 60 insertions(+), 44 deletions(-) diff --git a/js/components/ascribe_app.js b/js/components/ascribe_app.js index 6d159c26..2e05cc7b 100644 --- a/js/components/ascribe_app.js +++ b/js/components/ascribe_app.js @@ -22,10 +22,10 @@ let AscribeApp = React.createClass({ const { children, routes } = this.props; return ( -
    +
    - {/* Routes are injected here */} -
    +
    + {/* Routes are injected here */} {children}
    diff --git a/js/components/whitelabel/prize/portfolioreview/pr_app.js b/js/components/whitelabel/prize/portfolioreview/pr_app.js index 2fe1c2d6..9637c167 100644 --- a/js/components/whitelabel/prize/portfolioreview/pr_app.js +++ b/js/components/whitelabel/prize/portfolioreview/pr_app.js @@ -75,11 +75,12 @@ let PRApp = React.createClass({ } return ( -
    +
    {header} -
    +
    + {/* Routes are injected here */} {children}
    diff --git a/js/components/whitelabel/prize/simple_prize/prize_app.js b/js/components/whitelabel/prize/simple_prize/prize_app.js index f1779437..e40e8dc8 100644 --- a/js/components/whitelabel/prize/simple_prize/prize_app.js +++ b/js/components/whitelabel/prize/simple_prize/prize_app.js @@ -38,10 +38,12 @@ let PrizeApp = React.createClass({ } return ( -
    +
    {header} - {this.props.children} +
    + {/* Routes are injected here */} {children} +
    ); } diff --git a/js/components/whitelabel/wallet/components/23vivi/23vivi_landing.js b/js/components/whitelabel/wallet/components/23vivi/23vivi_landing.js index f6b2d50c..13368549 100644 --- a/js/components/whitelabel/wallet/components/23vivi/23vivi_landing.js +++ b/js/components/whitelabel/wallet/components/23vivi/23vivi_landing.js @@ -36,7 +36,7 @@ let Vivi23Landing = React.createClass({ render() { return ( -
    +
    diff --git a/js/components/whitelabel/wallet/components/cyland/cyland_landing.js b/js/components/whitelabel/wallet/components/cyland/cyland_landing.js index 21f8835a..95419bd4 100644 --- a/js/components/whitelabel/wallet/components/cyland/cyland_landing.js +++ b/js/components/whitelabel/wallet/components/cyland/cyland_landing.js @@ -57,38 +57,40 @@ let CylandLanding = React.createClass({ setDocumentTitle('CYLAND MediaArtLab'); return ( -
    -
    -
    -
    - -
    - {getLangText('Submissions to Cyland Archive are powered by') + ' '} - - - +
    +
    +
    +
    +
    + +
    + {getLangText('Submissions to Cyland Archive are powered by') + ' '} + + + +
    -
    -
    -
    -

    - {getLangText('Existing ascribe user?')} -

    - - - -
    -
    -

    - {getLangText('Do you need an account?')} -

    - - - +
    +
    +

    + {getLangText('Existing ascribe user?')} +

    + + + +
    +
    +

    + {getLangText('Do you need an account?')} +

    + + + +
    diff --git a/js/components/whitelabel/wallet/wallet_app.js b/js/components/whitelabel/wallet/wallet_app.js index 57088969..4735bdde 100644 --- a/js/components/whitelabel/wallet/wallet_app.js +++ b/js/components/whitelabel/wallet/wallet_app.js @@ -40,9 +40,10 @@ let WalletApp = React.createClass({ // In react-router 1.0, Routes have no 'name' property anymore. To keep functionality however, // we split the path by the first occurring slash and take the first splitter. return ( -
    -
    - {header} +
    + {header} +
    + {/* Routes are injected here */} {children}
    diff --git a/sass/whitelabel/wallet/23vivi/23vivi_custom_style.scss b/sass/whitelabel/wallet/23vivi/23vivi_custom_style.scss index a5026272..b6365dd2 100644 --- a/sass/whitelabel/wallet/23vivi/23vivi_custom_style.scss +++ b/sass/whitelabel/wallet/23vivi/23vivi_custom_style.scss @@ -20,8 +20,13 @@ $vivi23--highlight-color: #de2600; vertical-align: middle; } + .hero { + display: none; + } + .vivi23-landing { font-weight: normal; + padding: 0 15px; text-align: center; } diff --git a/sass/whitelabel/wallet/cyland/cyland_custom_style.scss b/sass/whitelabel/wallet/cyland/cyland_custom_style.scss index 9af18fcf..549b2004 100644 --- a/sass/whitelabel/wallet/cyland/cyland_custom_style.scss +++ b/sass/whitelabel/wallet/cyland/cyland_custom_style.scss @@ -178,8 +178,13 @@ $cyland--button-sec-color: #515151; vertical-align: middle; } + .hero { + display: none; + } + .cyland-landing { font-weight: normal; + padding: 0 15px; text-align: center; } } From 86ff1f88d03d9042ecc19bd622d49b8a81f62921 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Tue, 19 Jan 2016 15:15:33 +0100 Subject: [PATCH 35/94] Fix small issues --- js/components/error_not_found_page.js | 2 +- .../components/cyland/cyland_landing.js | 64 +++++++++---------- 2 files changed, 32 insertions(+), 34 deletions(-) diff --git a/js/components/error_not_found_page.js b/js/components/error_not_found_page.js index c42d2926..4ea0f4d1 100644 --- a/js/components/error_not_found_page.js +++ b/js/components/error_not_found_page.js @@ -21,7 +21,7 @@ let ErrorNotFoundPage = React.createClass({ componentDidMount() { // The previous page, if any, is the second item in the locationQueue - const { locationQueue: [ _, previousPage ] } = this.history; + const { locationQueue: [ , previousPage ] } = this.history; if (previousPage) { console.logGlobal('Page not found', { diff --git a/js/components/whitelabel/wallet/components/cyland/cyland_landing.js b/js/components/whitelabel/wallet/components/cyland/cyland_landing.js index 95419bd4..5f7cfeeb 100644 --- a/js/components/whitelabel/wallet/components/cyland/cyland_landing.js +++ b/js/components/whitelabel/wallet/components/cyland/cyland_landing.js @@ -57,40 +57,38 @@ let CylandLanding = React.createClass({ setDocumentTitle('CYLAND MediaArtLab'); return ( -
    -
    -
    -
    -
    - -
    - {getLangText('Submissions to Cyland Archive are powered by') + ' '} - - - -
    +
    +
    +
    +
    + +
    + {getLangText('Submissions to Cyland Archive are powered by') + ' '} + + +
    -
    -
    -

    - {getLangText('Existing ascribe user?')} -

    - - - -
    -
    -

    - {getLangText('Do you need an account?')} -

    - - - -
    +
    +
    +
    +

    + {getLangText('Existing ascribe user?')} +

    + + + +
    +
    +

    + {getLangText('Do you need an account?')} +

    + + +
    From 9c1b229d8580dc34dc3ea1b1cb11ae27599baf00 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Tue, 19 Jan 2016 15:06:26 +0100 Subject: [PATCH 36/94] Forgot to remove unnecessary import --- js/components/ascribe_app.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/js/components/ascribe_app.js b/js/components/ascribe_app.js index 2e05cc7b..8999246e 100644 --- a/js/components/ascribe_app.js +++ b/js/components/ascribe_app.js @@ -5,8 +5,6 @@ import React from 'react'; import AppBase from './app_base'; import Header from './header'; -import AppConstants from '../constants/application_constants'; - let AscribeApp = React.createClass({ propTypes: { From b8d6a79402d2ba814b4350eac8ebb265317cd8cc Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Wed, 20 Jan 2016 15:11:06 +0100 Subject: [PATCH 37/94] Update README for visual regression testing --- README.md | 69 ++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 50 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index e07eca0d..15736654 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,19 @@ Introduction ============ -Onion is the web client for Ascribe. The idea is to have a well documented, -easy to test, easy to hack, JavaScript application. +Onion is the web client for Ascribe. The idea is to have a well documented, modern, easy to test, easy to hack, JavaScript application. -The code is JavaScript ECMA 6. +The code is JavaScript 2015 / ECMAScript 6. Getting started =============== + Install some nice extension for Chrom(e|ium): - [React Developer Tools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi) - +- [Alt Developer Tools](https://github.com/goatslacker/alt-devtool) + ```bash git clone git@github.com:ascribe/onion.git cd onion @@ -37,17 +38,34 @@ Additionally, to work on the white labeling functionality, you need to edit your JavaScript Code Conventions =========================== + For this project, we're using: * 4 Spaces -* We use ES6 +* ES6 * We don't use ES6's class declaration for React components because it does not support Mixins as well as Autobinding ([Blog post about it](http://facebook.github.io/react/blog/2015/01/27/react-v0.13.0-beta-1.html#autobinding)) * We don't use camel case for file naming but in everything Javascript related -* We use `let` instead of `var`: [SA Post](http://stackoverflow.com/questions/762011/javascript-let-keyword-vs-var-keyword) -* We don't use Javascript's `Date` object, as its interface introduced bugs previously and we're including `momentjs` for other dependencies anyways +* We use `momentjs` instead of Javascript's `Date` object, as the native `Date` interface previously introduced bugs and we're including `momentjs` for other dependencies anyway + +Make sure to check out the [style guide](https://github.com/ascribe/javascript). + +Linting +------- + +We use [ESLint](https://github.com/eslint/eslint) with our own [custom ruleset](.eslintrc). + + +SCSS Code Conventions +===================== +Install [lint-scss](https://github.com/brigade/scss-lint), check the [editor integration docs](https://github.com/brigade/scss-lint#editor-integration) to integrate the lint in your editor. + +Some interesting links: +* [Improving Sass code quality on theguardian.com](https://www.theguardian.com/info/developer-blog/2014/may/13/improving-sass-code-quality-on-theguardiancom) + Branch names -===================== +============ + Since we moved to Github, we cannot create branch names automatically with JIRA anymore. To not lose context, but still be able to switch branches quickly using a ticket's number, we're recommending the following rules when naming our branches in onion. @@ -58,22 +76,21 @@ AD--brief-and-sane-description-of-the-ticket where `brief-and-sane-description-of-the-ticket` does not need to equal to the ticket's title. This allows JIRA to still track branches and pull-requests while allowing us to keep our peace of mind. + Example -------------- +------- + **JIRA ticket name:** `AD-1242 - Frontend caching for simple endpoints to measure perceived page load ` **Github branch name:** `AD-1242-caching-solution-for-stores` -SCSS Code Conventions -===================== -Install [lint-scss](https://github.com/brigade/scss-lint), check the [editor integration docs](https://github.com/brigade/scss-lint#editor-integration) to integrate the lint in your editor. - -Some interesting links: -* [Improving Sass code quality on theguardian.com](https://www.theguardian.com/info/developer-blog/2014/may/13/improving-sass-code-quality-on-theguardiancom) - Testing -=============== +======= + +Unit Testing +------------ + We're using Facebook's jest to do testing as it integrates nicely with react.js as well. Tests are always created per directory by creating a `__tests__` folder. To test a specific file, a `_tests.js` file needs to be created. @@ -83,7 +100,15 @@ This is due to the fact that jest's function mocking and ES6 module syntax are [ Therefore, to require a module in your test file, you need to use CommonJS's `require` syntax. Except for this, all tests can be written in ES6 syntax. -## Workflow +Visual Regression Testing +------------------------- + +We're using [Gemini](https://github.com/gemini-testing/gemini) for visual regression tests because it supports PhantomJS and SauceLabs. + + +Workflow +======== + Generally, when you're runing `gulp serve`, all tests are being run. If you want to test exclusively (without having the obnoxious ES6Linter warnings), you can just run `gulp jest:watch`. @@ -134,9 +159,16 @@ A: Easily by starting the your gulp process with the following command: ONION_BASE_URL='/' ONION_SERVER_URL='http://localhost.com:8000/' gulp serve ``` +Or, by adding these two your environment variables: +``` +ONION_BASE_URL='/' +ONION_SERVER_URL='http://localhost.com:8000/' +``` + Q: I want to know all dependencies that get bundled into the live build. A: ```browserify -e js/app.js --list > webapp-dependencies.txt``` + Reading list ============ @@ -149,7 +181,6 @@ Start here - [alt.js](http://alt.js.org/) - [alt.js readme](https://github.com/goatslacker/alt) - Moar stuff ---------- From 37f5432e7514707ca4e269eca41cb31b90d08b2b Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Wed, 20 Jan 2016 21:18:24 +0100 Subject: [PATCH 38/94] Add Gemini configuration for desktop and mobile on phantoms --- .gemini.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .gemini.yml diff --git a/.gemini.yml b/.gemini.yml new file mode 100644 index 00000000..5af1ef27 --- /dev/null +++ b/.gemini.yml @@ -0,0 +1,14 @@ +rootUrl: https://www.ascribe.ninja/app + +browsers: + PhantomJSDesktop: + screenshotsDir: './gemini/screens/desktop' + windowSize: 1900x1080 + desiredCapabilities: + browserName: phantomjs + + PhantomJSMobile: + screenshotsDir: './gemini/screens/mobile' + windowSize: 767x1364 + desiredCapabilities: + browserName: phantomjs From f44597c2bfa34afb1b8d4949e9b5267150c693eb Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Mon, 25 Jan 2016 10:59:44 +0100 Subject: [PATCH 39/94] Add docs for visual regression testing Also adds a simple script for testing with phantomJS --- .gitignore | 9 ++- README.md | 4 +- docs/visual-regression-testing.md | 126 ++++++++++++++++++++++++++++++ package.json | 4 +- phantomjs/launch_app_and_login.js | 62 +++++++++++++++ 5 files changed, 200 insertions(+), 5 deletions(-) create mode 100644 docs/visual-regression-testing.md create mode 100644 phantomjs/launch_app_and_login.js diff --git a/.gitignore b/.gitignore index 30c9eae9..17b63e95 100644 --- a/.gitignore +++ b/.gitignore @@ -16,9 +16,12 @@ webapp-dependencies.txt pids logs results - + +build/* + +gemini-coverage/* +gemini-report/* + node_modules/* -build - .DS_Store diff --git a/README.md b/README.md index 15736654..015c3fe9 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,9 @@ Therefore, to require a module in your test file, you need to use CommonJS's `re Visual Regression Testing ------------------------- -We're using [Gemini](https://github.com/gemini-testing/gemini) for visual regression tests because it supports PhantomJS and SauceLabs. +We're using [Gemini](https://github.com/gemini-testing/gemini) for visual regression tests because it supports both PhantomJS2 and SauceLabs. + +See the [helper docs](docs/visual-regression-testing.md) for information on installing Gemini, its dependencies, and running and writing tests. Workflow diff --git a/docs/visual-regression-testing.md b/docs/visual-regression-testing.md new file mode 100644 index 00000000..30a711ff --- /dev/null +++ b/docs/visual-regression-testing.md @@ -0,0 +1,126 @@ +Introduction +============ + +When in doubt, see [Gemini](https://github.com/gemini-testing/gemini) and [their docs](https://github.com/gemini-testing/gemini/tree/master/doc) for more information as well as configuration options. + +Contents +======== + + 1. [Installation](#installation) + 1. [Running Tests](#running-tests) + 1. [Gemini Usage](#gemini-usage) + 1. [PhantomJS](#phantomjs) + + +Installation +============ + +First install [PhantomJS2](https://www.npmjs.com/package/phantomjs2): + +```bash +npm install -g phantomjs2 + +# If using OSX, install upx and decompress the binary downloaded by npm manually: +brew install upx +# Navigate to the binary, ie. /Users/Brett/.nvm/v5.4.0/lib/node_modules/phantomjs2/lib/phantom/bin +upx -d phantomjs + +``` + +Then [install Gemini globally and locally with npm](https://github.com/gemini-testing/gemini/blob/master/README.md#installation). + + +Running Tests +============= + +Run PhantomJS: + +```bash +phantomjs --webdriver=4444 +``` + +And then run Gemini tests: + +```bash +# In root onion/ +gemini test gemini/* --report html +``` + +If you've made changes and want them to be the new baseline (ie. it's a correct change--**make sure** to test there are no regressions first!), use + +```bash +# In root onion/ +gemini update gemini/* +``` + + +Gemini Usage +============ + +While Gemini itself is easy to use on simple, static pages, there are some nice to knows when dealing with a single page app like ours (where much of it is behind an authentication barrier as well). + +Authentication +-------------- + +Authentication presents a tricky problem with Gemini, since we can't inject any cookies or even run a start up script through the browser before letting Gemini hook in. The solution is to script the log in process through Gemini, and put waits for the log in to succeed, before testing parts of the app that require the authentication. + +Browser Session States +---------------------- + +Gemini will start a new instance of the browser for each browser configuration defined in the .gemini.yml file when Gemini's launched (ie. `gemini update`, `gemini test`, etc). + +Although each new suite will cause the testing browser to be refreshed, the above means that cookies and other persistent state will be kept across suites for a browser across all runs, even if the suites are from different files. + +**What this comes down to is**: once you've logged in, you'll stay logged in until you decide to log out or the running instance of Gemini ends. In general practice, it's a good idea to clear the state of the app at the end of each suite of tests by logging out. + +(**Note**: Persistent storage, such as local storage, has not been explicitly tested as to whether they are kept, but as the cookies are cleared each time, this seems unlikely) + +Test Reporting +-------------- + +Using the `--report html` flag with Gemini will produce a webpage with the test's results in /gemini-report that will show the old, new, and diff images. Using this is highly recommended (and fun!). + + +PhantomJS +========= + +[PhantomJS](http://phantomjs.org/) is a headless browser that allows us to run tests and take screenshots without needing a browser. + +Its second version (PhantomJS2) uses a much more recent version of Webkit, and is a big reason why Gemini (as opposed to other utilities, ie. PhantomCSS) was chosen. Due to the large number of breaking changes introduced between PhantomJS 1.9 to 2.0, a large number of tools (ie. CasperJS) are, at the time of writing, lacking support for 2.0. + +While you don't need to know too much about PhantomJS to use and write Gemini tests, there are still a number of useful things to know about. + +Useful features +--------------- + +You can find the full list of CLI commands in the [documentation](http://phantomjs.org/api/command-line.html). + +Flags that are of particular interest to us: + * `--webdriver=4444`: sets the webdriver port to be 4444, the default webdriver port that Gemini expects. + * `--ignore-ssl-errors=true`: ignores any SSL errors that may occur. Particular useful when hooking up the tests to staging, as the certificate we use is self-signed. + * `--ssl-protocol=any`: allows any ssl protocol to be used. May be useful when `--ignore-ssl-errors=true` doesn't work. + * '--remote-debugger-port`: allows for remote debugging the running PhantomJS instance. More on this later. + +Troubleshooting and Debugging +----------------------------- + +Remote debugging is possible with PhantomJS using the `--remote-debugger-port` option. See the [troubleshooting docs](http://phantomjs.org/troubleshooting.html). + +To begin using it, add `debugger;` statements to the file being run by `phantomjs`, and access the port number specified after `--remote-debugger-port` on localhost: + +```bash +phantomjs --remote-debugger-port=9000 debug.js +``` + +PhantomJS will start and then immediately breakpoint. Go to http://localhost:9000/webkit/inspector/inspector.html?page=1 and then to its console tab. Go to your first breakpoint (the first `debugger;` statement executed) by running `__run()` in the console tab. Subsequent breakpoints can be reached by successively running `__run()` in that same console tab. + +At each breakpoint, you can to http://localhost:9000 on a new browser tab and click on one of the links to go to the current execution state of that breakpoint on the page you're on. + +--- + +To simplify triaging simple issues and test if everything is working, I've added a short test script that can be run with PhantomJS to check if it can access the web app and log in. You can edit the `lauch_app_and_login.js` file to change the environment to run against. + +```bash +# In root /onion folder +phantomjs phantomjs/launch_app_and_login.js +``` diff --git a/package.json b/package.json index c961e9c3..d3e7ae41 100644 --- a/package.json +++ b/package.json @@ -35,8 +35,10 @@ "devDependencies": { "babel-eslint": "^3.1.11", "babel-jest": "^5.2.0", + "gemini": "^2.0.3", "gulp-sass": "^2.1.1", - "jest-cli": "^0.4.0" + "jest-cli": "^0.4.0", + "phantomjs2": "^2.0.2" }, "dependencies": { "alt": "^0.16.5", diff --git a/phantomjs/launch_app_and_login.js b/phantomjs/launch_app_and_login.js new file mode 100644 index 00000000..e5418519 --- /dev/null +++ b/phantomjs/launch_app_and_login.js @@ -0,0 +1,62 @@ +'use strict'; + +var liveEnv = 'https://www.ascribe.io/app/login'; +// Note that if you are trying to access staging, you will need to use +// the --ignore-ssl-errors=true flag on phantomjs +var stagingEnv = 'https://www.ascribe.ninja/app/login'; +var localEnv = 'http://localhost.com:3000/login'; + +var page = require('webpage').create(); +page.open(localEnv, function(status) { + var attemptedToLogIn; + var loginCheckInterval; + + console.log('Status: ' + status); + + if (status === 'success') { + console.log('Attempting to log in...'); + + attemptedToLogIn = page.evaluate(function () { + try { + var inputForm = document.querySelector('.ascribe-login-wrapper'); + var email = inputForm.querySelector('input[type=email]'); + var password = inputForm.querySelector('input[type=password]'); + var submitBtn = inputForm.querySelector('button[type=submit]'); + + email.value = 'dimi@mailinator.com'; + password.value = '0000000000'; + submitBtn.click(); + + return true; + } catch (ex) { + console.log('Error while trying to find login elements, not logging in.'); + return false; + } + }); + + if (attemptedToLogIn) { + loginCheckInterval = setInterval(function () { + var loggedIn = page.evaluate(function () { + // When they log in, they are taken to the collections page. + // When the piece list is loaded, the accordion list is either available or + // shows a placeholder, so let's check for these elements to determine + // when login is finished + return !!(document.querySelector('.ascribe-accordion-list:not(.ascribe-loading-position)') || + document.querySelector('.ascribe-accordion-list-placeholder')); + }); + + if (loggedIn) { + clearInterval(loginCheckInterval); + console.log('Successfully logged in.'); + } + }, 1000); + } else { + console.log('Something happened while trying to log in, aborting...'); + phantom.exit(); + } + + } else { + console.log('Failed to load page, exiing...'); + phantom.exit(); + } +}); From 7edddc4d5a86867834ac3582382f9de92995df67 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Fri, 29 Jan 2016 11:17:30 +0100 Subject: [PATCH 40/94] Update docs for PhantomJS 2.1, whitespace, and more tips --- docs/visual-regression-testing.md | 107 +++++++++++++++++++++++------- 1 file changed, 82 insertions(+), 25 deletions(-) diff --git a/docs/visual-regression-testing.md b/docs/visual-regression-testing.md index 30a711ff..d544b1fa 100644 --- a/docs/visual-regression-testing.md +++ b/docs/visual-regression-testing.md @@ -1,33 +1,39 @@ Introduction ============ -When in doubt, see [Gemini](https://github.com/gemini-testing/gemini) and [their docs](https://github.com/gemini-testing/gemini/tree/master/doc) for more information as well as configuration options. +When in doubt, see [Gemini](https://github.com/gemini-testing/gemini) and [their +docs](https://github.com/gemini-testing/gemini/tree/master/doc) for more information as well as configuration options. Contents ======== 1. [Installation](#installation) 1. [Running Tests](#running-tests) - 1. [Gemini Usage](#gemini-usage) + 1. [Gemini Usage and Writing Tests](#gemini-usage-and-writing-tests) 1. [PhantomJS](#phantomjs) + 1. [TODO](#todo) Installation ============ -First install [PhantomJS2](https://www.npmjs.com/package/phantomjs2): +First make sure that you're using NodeJS 5.0+ as the tests are written using ES6 syntax. + +Then, install [PhantomJS2](https://www.npmjs.com/package/phantomjs2): ```bash -npm install -g phantomjs2 +# Until phantomjs2 is updated for the new 2.1 version of PhantomJS, use the following (go to https://bitbucket.org/ariya/phantomjs/downloads to find a build for your OS) +npm install -g phantomjs2 --phantomjs_downloadurl=https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-macosx.zip -# If using OSX, install upx and decompress the binary downloaded by npm manually: +# If using OSX, you may have to install upx and decompress the binary downloaded by npm manually: brew install upx -# Navigate to the binary, ie. /Users/Brett/.nvm/v5.4.0/lib/node_modules/phantomjs2/lib/phantom/bin + +# Navigate to the binary, ie. /Users/Brett/.nvm/versions/node/v5.4.0/lib/node_modules/phantomjs2/lib/phantom/bin/phantomjs upx -d phantomjs ``` -Then [install Gemini globally and locally with npm](https://github.com/gemini-testing/gemini/blob/master/README.md#installation). +Finally, [install Gemini globally and locally with npm](https://github.com/gemini-testing/gemini/blob/master/README.md#installation). Running Tests @@ -54,41 +60,84 @@ gemini update gemini/* ``` -Gemini Usage -============ +Gemini Usage and Writing Tests +============================== -While Gemini itself is easy to use on simple, static pages, there are some nice to knows when dealing with a single page app like ours (where much of it is behind an authentication barrier as well). +While Gemini itself is easy to use on simple, static pages, there are some nice to knows when dealing with a single page +app like ours (where much of it is behind an authentication barrier as well). + +Command Line Interface +---------------------- + +See [the docs](https://github.com/gemini-testing/gemini/blob/master/doc/commands.md) on the commands that are available. +`npm run vi-*` is set up with some of these commands, but you may want to build your own or learn about some of the +other functions. Authentication -------------- -Authentication presents a tricky problem with Gemini, since we can't inject any cookies or even run a start up script through the browser before letting Gemini hook in. The solution is to script the log in process through Gemini, and put waits for the log in to succeed, before testing parts of the app that require the authentication. +Authentication presents a tricky problem with Gemini, since we can't inject any cookies or even run a start up script +through the browser before letting Gemini hook in. The solution is to script the log in process through Gemini, and put +waits for the log in to succeed, before testing parts of the app that require the authentication. Browser Session States ---------------------- -Gemini will start a new instance of the browser for each browser configuration defined in the .gemini.yml file when Gemini's launched (ie. `gemini update`, `gemini test`, etc). +Gemini will start a new instance of the browser for each browser configuration defined in the .gemini.yml file when +Gemini's launched (ie. `gemini update`, `gemini test`, etc). -Although each new suite will cause the testing browser to be refreshed, the above means that cookies and other persistent state will be kept across suites for a browser across all runs, even if the suites are from different files. +Although each new suite will cause the testing browser to be refreshed, the above means that cookies and other +persistent state will be kept across suites for a browser across all runs, even if the suites are from different files. -**What this comes down to is**: once you've logged in, you'll stay logged in until you decide to log out or the running instance of Gemini ends. In general practice, it's a good idea to clear the state of the app at the end of each suite of tests by logging out. +**What this comes down to is**: once you've logged in, you'll stay logged in until you decide to log out or the running +instance of Gemini ends. In general practice, it's a good idea to clear the state of the app at the end of each suite of +tests by logging out. -(**Note**: Persistent storage, such as local storage, has not been explicitly tested as to whether they are kept, but as the cookies are cleared each time, this seems unlikely) +(**Note**: Persistent storage, such as local storage, has not been explicitly tested as to whether they are kept, but as +the cookies are cleared each time, this seems unlikely) Test Reporting -------------- -Using the `--report html` flag with Gemini will produce a webpage with the test's results in /gemini-report that will show the old, new, and diff images. Using this is highly recommended (and fun!). +Using the `--reporter html` flag with Gemini will produce a webpage with the test's results in `onion/gemini-report` +that will show the old, new, and diff images. Using this is highly recommended (and fun!) and is used by default in `npm +run vi-test`. +Writing Tests +------------- + +See [the docs](https://github.com/gemini-testing/gemini/blob/master/doc/tests.md), and the [section on the available +actions](https://github.com/gemini-testing/gemini/blob/master/doc/tests.md#available-actions) for what scripted actions +are available. + +Our tests are located in `onion/gemini/`. + +Some useful tips: + * The `find()` method in the callbacks is equivalent to `document.querySelector`; it will only return the first + element found that matches the selector. Use pseudo classes like `nth-of-type()`, `nth-child()`, and etc. to select + later elements. + * Nested suites inherit from their parent suites' configurations, but will **override** their inherited configuration + if another is specified. For example, if `parentSuite` had a `.before()` method, all children of `parentSuite` would + run its `.before()`, but if any of the children specified their own `.before()`, those children would **not** run + `parentSuite`'s `.before()`. + * Gemini takes a screenshot of the minimum bounding rect for all specified selectors, so this means you can't take a + screenshot of two items far away from each other without the rest being considered (ie. trying to get the header and + footer) + * Unfortunately, `setCaptureElements` and `ignoreElements` will only apply for the first element found matching those + selectors. PhantomJS ========= -[PhantomJS](http://phantomjs.org/) is a headless browser that allows us to run tests and take screenshots without needing a browser. +[PhantomJS](http://phantomjs.org/) is a headless browser that allows us to run tests and take screenshots without +needing a browser. -Its second version (PhantomJS2) uses a much more recent version of Webkit, and is a big reason why Gemini (as opposed to other utilities, ie. PhantomCSS) was chosen. Due to the large number of breaking changes introduced between PhantomJS 1.9 to 2.0, a large number of tools (ie. CasperJS) are, at the time of writing, lacking support for 2.0. +Its second version (PhantomJS2) uses a much more recent version of Webkit, and is a big reason why Gemini (as opposed to +other utilities, ie. PhantomCSS) was chosen. Due to the large number of breaking changes introduced between PhantomJS +1.9 to 2.0, a large number of tools (ie. CasperJS) are, at the time of writing, lacking support for 2.0. -While you don't need to know too much about PhantomJS to use and write Gemini tests, there are still a number of useful things to know about. +While you don't need to know too much about PhantomJS to use and write Gemini tests, there are still a number of useful +things to know about. Useful features --------------- @@ -97,28 +146,36 @@ You can find the full list of CLI commands in the [documentation](http://phantom Flags that are of particular interest to us: * `--webdriver=4444`: sets the webdriver port to be 4444, the default webdriver port that Gemini expects. - * `--ignore-ssl-errors=true`: ignores any SSL errors that may occur. Particular useful when hooking up the tests to staging, as the certificate we use is self-signed. + * `--ignore-ssl-errors=true`: ignores any SSL errors that may occur. Particular useful when hooking up the tests to + staging, as the certificate we use is self-signed. * `--ssl-protocol=any`: allows any ssl protocol to be used. May be useful when `--ignore-ssl-errors=true` doesn't work. * '--remote-debugger-port`: allows for remote debugging the running PhantomJS instance. More on this later. Troubleshooting and Debugging ----------------------------- -Remote debugging is possible with PhantomJS using the `--remote-debugger-port` option. See the [troubleshooting docs](http://phantomjs.org/troubleshooting.html). +Remote debugging is possible with PhantomJS using the `--remote-debugger-port` option. See the [troubleshooting +docs](http://phantomjs.org/troubleshooting.html). -To begin using it, add `debugger;` statements to the file being run by `phantomjs`, and access the port number specified after `--remote-debugger-port` on localhost: +To begin using it, add `debugger;` statements to the file being run by `phantomjs`, and access the port number specified +after `--remote-debugger-port` on localhost: ```bash phantomjs --remote-debugger-port=9000 debug.js ``` -PhantomJS will start and then immediately breakpoint. Go to http://localhost:9000/webkit/inspector/inspector.html?page=1 and then to its console tab. Go to your first breakpoint (the first `debugger;` statement executed) by running `__run()` in the console tab. Subsequent breakpoints can be reached by successively running `__run()` in that same console tab. +PhantomJS will start and then immediately breakpoint. Go to http://localhost:9000/webkit/inspector/inspector.html?page=1 +and then to its console tab. Go to your first breakpoint (the first `debugger;` statement executed) by running `__run()` +in the console tab. Subsequent breakpoints can be reached by successively running `__run()` in that same console tab. -At each breakpoint, you can to http://localhost:9000 on a new browser tab and click on one of the links to go to the current execution state of that breakpoint on the page you're on. +At each breakpoint, you can to http://localhost:9000 on a new browser tab and click on one of the links to go to the +current execution state of that breakpoint on the page you're on. --- -To simplify triaging simple issues and test if everything is working, I've added a short test script that can be run with PhantomJS to check if it can access the web app and log in. You can edit the `lauch_app_and_login.js` file to change the environment to run against. +To simplify triaging simple issues and test if everything is working, I've added a short test script that can be run +with PhantomJS to check if it can access the web app and log in. You can edit the `lauch_app_and_login.js` file to +change the environment to run against. ```bash # In root /onion folder From 01217318a437c296001240bd0f1527ca28137f27 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Fri, 29 Jan 2016 11:20:27 +0100 Subject: [PATCH 41/94] Add ids to dropdown buttons, as per warnings Also helps us find these dropdown buttons with Gemini. --- .../piece_list_toolbar_filter_widget.js | 1 + .../piece_list_toolbar_order_widget.js | 7 ++++--- js/components/header.js | 1 + js/components/header_notification.js | 1 + js/components/nav_routes_links_link.js | 6 ++++-- 5 files changed, 11 insertions(+), 5 deletions(-) diff --git a/js/components/ascribe_piece_list_toolbar/piece_list_toolbar_filter_widget.js b/js/components/ascribe_piece_list_toolbar/piece_list_toolbar_filter_widget.js index edb29e85..c9791dbe 100644 --- a/js/components/ascribe_piece_list_toolbar/piece_list_toolbar_filter_widget.js +++ b/js/components/ascribe_piece_list_toolbar/piece_list_toolbar_filter_widget.js @@ -84,6 +84,7 @@ let PieceListToolbarFilterWidget = React.createClass({ if (this.props.filterParams && this.props.filterParams.length) { return ( diff --git a/js/components/ascribe_piece_list_toolbar/piece_list_toolbar_order_widget.js b/js/components/ascribe_piece_list_toolbar/piece_list_toolbar_order_widget.js index 5257cc07..da9bae43 100644 --- a/js/components/ascribe_piece_list_toolbar/piece_list_toolbar_order_widget.js +++ b/js/components/ascribe_piece_list_toolbar/piece_list_toolbar_order_widget.js @@ -45,7 +45,7 @@ let PieceListToolbarOrderWidget = React.createClass({ }, render() { - let filterIcon = ( + let orderIcon = ( · @@ -55,9 +55,10 @@ let PieceListToolbarOrderWidget = React.createClass({ if (this.props.orderParams && this.props.orderParams.length) { return ( + className="ascribe-piece-list-toolbar-filter-widget" + title={orderIcon}>
  • {getLangText('Sort by')}:
  • diff --git a/js/components/header.js b/js/components/header.js index c16cba86..4bb6881e 100644 --- a/js/components/header.js +++ b/js/components/header.js @@ -163,6 +163,7 @@ let Header = React.createClass({ account = ( diff --git a/js/components/nav_routes_links_link.js b/js/components/nav_routes_links_link.js index 11235ccd..e2bfb7ed 100644 --- a/js/components/nav_routes_links_link.js +++ b/js/components/nav_routes_links_link.js @@ -29,7 +29,9 @@ let NavRoutesLinksLink = React.createClass({ // with MenuItems if(children) { return ( - + {children} ); @@ -55,4 +57,4 @@ let NavRoutesLinksLink = React.createClass({ } }); -export default NavRoutesLinksLink; \ No newline at end of file +export default NavRoutesLinksLink; From 7383fbb1f648214a838d0854bcd45fa984658b9d Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Fri, 29 Jan 2016 11:20:43 +0100 Subject: [PATCH 42/94] Standardize CoaVerify route across apps --- js/components/coa_verify_container.js | 85 +++++++++---------- .../whitelabel/wallet/wallet_routes.js | 10 +-- 2 files changed, 46 insertions(+), 49 deletions(-) diff --git a/js/components/coa_verify_container.js b/js/components/coa_verify_container.js index 6d0af0fa..4a270cf4 100644 --- a/js/components/coa_verify_container.js +++ b/js/components/coa_verify_container.js @@ -27,7 +27,7 @@ let CoaVerifyContainer = React.createClass({ return (
    -
    +
    {getLangText('Verify your Certificate of Authenticity')}
    @@ -37,7 +37,7 @@ let CoaVerifyContainer = React.createClass({ signature={signature}/>

    - {getLangText('ascribe is using the following public key for verification')}: + {getLangText('ascribe is using the following public key for verification')}:
                     -----BEGIN PUBLIC KEY-----
    @@ -60,9 +60,8 @@ let CoaVerifyForm = React.createClass({
         },
     
         handleSuccess(response){
    -        let notification = null;
             if (response.verdict) {
    -            notification = new GlobalNotificationModel(getLangText('Certificate of Authenticity successfully verified'), 'success');
    +            const notification = new GlobalNotificationModel(getLangText('Certificate of Authenticity successfully verified'), 'success');
                 GlobalNotificationActions.appendGlobalNotification(notification);
             }
         },
    @@ -71,46 +70,44 @@ let CoaVerifyForm = React.createClass({
             const { message, signature } = this.props;
     
             return (
    -            
    - - {getLangText('Verify your Certificate of Authenticity')} - } - spinner={ - - - - }> - - - - - - -
    - -
    +
    + {getLangText('Verify your Certificate of Authenticity')} + + } + spinner={ + + + + }> + + + + + + +
    +
    ); } }); diff --git a/js/components/whitelabel/wallet/wallet_routes.js b/js/components/whitelabel/wallet/wallet_routes.js index a5a0e075..b4e3c7ea 100644 --- a/js/components/whitelabel/wallet/wallet_routes.js +++ b/js/components/whitelabel/wallet/wallet_routes.js @@ -77,7 +77,7 @@ let ROUTES = { component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(CylandPieceList)} headerTitle='COLLECTION' /> - + @@ -112,7 +112,7 @@ let ROUTES = { headerTitle='COLLECTION' /> - + ), @@ -156,7 +156,7 @@ let ROUTES = { component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(IkonotvContractNotifications)} /> - + ), @@ -192,7 +192,7 @@ let ROUTES = { headerTitle='COLLECTION' /> - + ), @@ -228,7 +228,7 @@ let ROUTES = { headerTitle='COLLECTION' /> - + ) From f9c70e2beb87af5fdcbc637cb5c65d4afd4d2398 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Fri, 29 Jan 2016 11:21:57 +0100 Subject: [PATCH 43/94] Main app visual test suite --- .gemini.yml | 15 ++- gemini/main/authenticated.js | 214 +++++++++++++++++++++++++++++++++++ gemini/main/basic.js | 145 ++++++++++++++++++++++++ gemini/main/detail.js | 125 ++++++++++++++++++++ 4 files changed, 493 insertions(+), 6 deletions(-) create mode 100644 gemini/main/authenticated.js create mode 100644 gemini/main/basic.js create mode 100644 gemini/main/detail.js diff --git a/.gemini.yml b/.gemini.yml index 5af1ef27..8d434624 100644 --- a/.gemini.yml +++ b/.gemini.yml @@ -1,14 +1,17 @@ -rootUrl: https://www.ascribe.ninja/app +rootUrl: http://localhost.com:3000/ +sessionsPerBrowser: 1 browsers: - PhantomJSDesktop: - screenshotsDir: './gemini/screens/desktop' + MainDesktop: + rootUrl: http://localhost.com:3000/ + screenshotsDir: './gemini-screens/desktop' windowSize: 1900x1080 desiredCapabilities: browserName: phantomjs - PhantomJSMobile: - screenshotsDir: './gemini/screens/mobile' - windowSize: 767x1364 + MainMobile: + rootUrl: http://localhost.com:3000/ + screenshotsDir: './gemini-screens/mobile' + windowSize: 600x1056 desiredCapabilities: browserName: phantomjs diff --git a/gemini/main/authenticated.js b/gemini/main/authenticated.js new file mode 100644 index 00000000..bf579bff --- /dev/null +++ b/gemini/main/authenticated.js @@ -0,0 +1,214 @@ +'use strict'; + +const gemini = require('gemini'); + +/** + * Suite of tests against routes that require the user to be authenticated. +*/ +gemini.suite('Authenticated', (suite) => { + suite + .setUrl('/collection') + .setCaptureElements('.ascribe-body') + .before((actions, find) => { + // This will be called before every nested suite begins unless that suite + // also defines a `.before()` + // FIXME: use a more generic class for this, like just '.app', + // when we can use this file with the whitelabels + actions.waitForElementToShow('.ascribe-default-app', 5000); + }); + + // Suite just to log us in before any other suites run + gemini.suite('Login', (loginSuite) => { + loginSuite + .setUrl('/login') + .ignoreElements('.ascribe-body') + .capture('logged in', (actions, find) => { + actions.waitForElementToShow('.ascribe-form', 5000); + + actions.sendKeys(find('.ascribe-login-wrapper input[name=email]'), 'dimi@mailinator.com'); + actions.sendKeys(find('.ascribe-login-wrapper input[name=password]'), '0000000000'); + actions.click(find('.ascribe-login-wrapper button[type=submit]')); + + actions.waitForElementToShow('.ascribe-accordion-list:not(.ascribe-loading-position)', 5000); + }); + }); + + gemini.suite('Header-desktop', (headerSuite) => { + headerSuite + .setCaptureElements('nav.navbar .container') + // Ignore Cyland's logo as it's a gif + .ignoreElements('.client--cyland img.img-brand') + .skip(/Mobile/) + .before((actions, find) => { + actions.waitForElementToShow('.ascribe-accordion-list:not(.ascribe-loading-position)', 5000); + }) + .capture('desktop header'); + + gemini.suite('User dropdown', (headerUserSuite) => { + headerUserSuite + .setCaptureElements('#nav-route-user-dropdown ~ .dropdown-menu') + .capture('expanded user dropdown', (actions, find) => { + actions.click(find('#nav-route-user-dropdown')); + }); + }); + + gemini.suite('Notification dropdown', (headerNotificationSuite) => { + headerNotificationSuite + .setCaptureElements('#header-notification-dropdown ~ .dropdown-menu') + .capture('expanded notifications dropdown', (actions, find) => { + actions.click(find('#header-notification-dropdown')); + }); + }); + }); + + // Test for the collapsed header in mobile + gemini.suite('Header-mobile', (headerMobileSuite) => { + headerMobileSuite + .setCaptureElements('nav.navbar .container') + // Ignore Cyland's logo as it's a gif + .ignoreElements('.client--cyland img.img-brand') + .skip(/Desktop/) + .before((actions, find) => { + actions.waitForElementToShow('.ascribe-accordion-list:not(.ascribe-loading-position)', 5000); + }) + .capture('mobile header') + .capture('expanded mobile header', (actions, find) => { + actions.click(find('nav.navbar .navbar-toggle')); + // Wait for the header to expand + actions.wait(500); + }) + .capture('expanded user dropdown', (actions, find) => { + actions.click(find('#nav-route-user-dropdown')); + }) + .capture('expanded notifications dropdown', (actions, find) => { + actions.click(find('#header-notification-dropdown')); + }); + }); + + gemini.suite('Collection', (collectionSuite) => { + collectionSuite + .setCaptureElements('.ascribe-accordion-list') + .before((actions, find) => { + actions.waitForElementToShow('.ascribe-accordion-list:not(.ascribe-loading-position)', 5000); + // Wait for the images to load + // FIXME: unfortuntately gemini doesn't support ignoring multiple elements from a single selector + // so we're forced to wait and hope that the images will all finish loading after 5s. + // We could also change the thumbnails with JS, but setting up a test user is probably easier. + actions.wait(5000); + }) + .capture('collection') + .capture('expanded edition in collection', (actions, find) => { + actions.click(find('.ascribe-accordion-list-item .ascribe-accordion-list-item-edition-widget')); + // Wait for editions to load + actions.waitForElementToShow('.ascribe-accordion-list-item-table', 5000); + }) + + gemini.suite('Collection placeholder', (collectionPlaceholderSuite) => { + collectionPlaceholderSuite + .setCaptureElements('.ascribe-accordion-list-placeholder') + .capture('collection empty search', (actions, find) => { + actions.sendKeys(find('.ascribe-piece-list-toolbar .search-bar input[type="text"]'), 'no search result'); + actions.waitForElementToShow('.ascribe-accordion-list-placeholder', 5000); + }); + }); + + gemini.suite('PieceListBulkModal', (pieceListBulkModalSuite) => { + pieceListBulkModalSuite + .setCaptureElements('.piece-list-bulk-modal') + .capture('items selected', (actions, find) => { + actions.click(find('.ascribe-accordion-list-item .ascribe-accordion-list-item-edition-widget')); + // Wait for editions to load + actions.waitForElementToShow('.ascribe-accordion-list-item-table', 5000); + + actions.click('.ascribe-table thead tr input[type="checkbox"]'); + actions.waitForElementToShow('.piece-list-bulk-modal'); + }); + }); + }); + + gemini.suite('PieceListToolbar', (pieceListToolbarSuite) => { + pieceListToolbarSuite + .setCaptureElements('.ascribe-piece-list-toolbar') + .capture('piece list toolbar') + .capture('piece list toolbar search filled', (actions, find) => { + actions.sendKeys(find('.ascribe-piece-list-toolbar .search-bar input[type="text"]'), 'search text'); + }) + + gemini.suite('Order widget dropdown', (pieceListToolbarOrderWidgetSuite) => { + pieceListToolbarOrderWidgetSuite + .setCaptureElements('#ascribe-piece-list-toolbar-order-widget-dropdown', + '#ascribe-piece-list-toolbar-order-widget-dropdown ~ .dropdown-menu') + .capture('expanded order dropdown', (actions, find) => { + actions.click(find('#ascribe-piece-list-toolbar-order-widget-dropdown')); + + // Wait as the dropdown screenshot still includes the collection in the background + actions.waitForElementToShow('.ascribe-accordion-list:not(.ascribe-loading-position)', 5000); + }); + }); + + gemini.suite('Filter widget dropdown', (pieceListToolbarFilterWidgetSuite) => { + pieceListToolbarFilterWidgetSuite + .setCaptureElements('#ascribe-piece-list-toolbar-filter-widget-dropdown', + '#ascribe-piece-list-toolbar-filter-widget-dropdown ~ .dropdown-menu') + .capture('expanded filter dropdown', (actions, find) => { + actions.click(find('#ascribe-piece-list-toolbar-filter-widget-dropdown')); + + // Wait as the dropdown screenshot still includes the collection in the background + actions.waitForElementToShow('.ascribe-accordion-list:not(.ascribe-loading-position)', 5000); + }); + }); + }); + + gemini.suite('Register work', (registerSuite) => { + registerSuite + .setUrl('/register_piece') + .capture('register work', (actions, find) => { + // The uploader options are only rendered after the user is fetched, so + // we have to wait for it here + actions.waitForElementToShow('.file-drag-and-drop-dialog .present-options', 5000); + }) + .capture('register work filled', (actions, find) => { + actions.sendKeys(find('.ascribe-form input[name="artist_name"]'), 'artist name'); + actions.sendKeys(find('.ascribe-form input[name="title"]'), 'title'); + actions.sendKeys(find('.ascribe-form input[name="date_created"]'), 'date created'); + }) + .capture('register work filled with editions', (actions, find) => { + actions.click(find('.ascribe-form input[type="checkbox"] ~ .checkbox')); + actions.wait(500); + actions.sendKeys(find('.ascribe-form input[name="num_editions"]'), '50'); + }); + + gemini.suite('Register work hash', (registerHashSuite) => { + registerHashSuite + .setUrl('/register_piece?method=hash') + .capture('register work hash method'); + }); + + gemini.suite('Register work upload', (registerUploadSuite) => { + registerUploadSuite + .setUrl('/register_piece?method=upload') + .capture('register work upload method'); + }); + }); + + gemini.suite('User settings', (userSettingsSuite) => { + userSettingsSuite + .setUrl('/settings') + .before((actions, find) => { + // This will be called before every nested suite begins unless that suite + // also defines a `.before()` + actions.waitForElementToShow('.settings-container', 5000); + }) + .capture('user settings'); + }); + + // Suite just to log out after suites have run + gemini.suite('Log out', (logoutSuite) => { + logoutSuite + .setUrl('/logout') + .ignoreElements('.ascribe-body') + .capture('logout', (actions, find) => { + actions.waitForElementToShow('.ascribe-login-wrapper', 10000); + }); + }); +}); diff --git a/gemini/main/basic.js b/gemini/main/basic.js new file mode 100644 index 00000000..317c5d84 --- /dev/null +++ b/gemini/main/basic.js @@ -0,0 +1,145 @@ +'use strict'; + +const gemini = require('gemini'); + +/** + * Basic suite of tests against routes that do not require the user to be authenticated. +*/ +gemini.suite('Basic', (suite) => { + suite + .setUrl('/login') + .setCaptureElements('.ascribe-body') + .before((actions, find) => { + // This will be called before every nested suite begins unless that suite + // also defines a `.before()` + // FIXME: use a more generic class for this, like just '.ascribe-app' + actions.waitForElementToShow('.ascribe-default-app', 5000); + }); + + gemini.suite('Header-desktop', (headerSuite) => { + headerSuite + .setCaptureElements('nav.navbar .container') + .skip(/Mobile/) + .capture('desktop header', (actions, find) => { + actions.waitForElementToShow('nav.navbar .container', 5000); + }) + .capture('hover on active item', (actions, find) => { + const activeItem = find('nav.navbar li.active'); + actions.mouseMove(activeItem); + }) + .capture('hover on inactive item', (actions, find) => { + const inactiveItem = find('nav.navbar li:not(.active)'); + actions.mouseMove(inactiveItem); + }); + }); + + // Test for the collapsed header in mobile + gemini.suite('Header-mobile', (headerMobileSuite) => { + headerMobileSuite + .setCaptureElements('nav.navbar .container') + .skip(/Desktop/) + .capture('mobile header', (actions, find) => { + actions.waitForElementToShow('nav.navbar .container', 5000); + }) + .capture('expanded mobile header', (actions, find) => { + actions.click(find('nav.navbar .navbar-toggle')); + // Wait for the header to expand + actions.wait(500); + }) + .capture('hover on expanded mobile header item', (actions, find) => { + actions.mouseMove(find('nav.navbar li')); + }); + }); + + gemini.suite('Footer', (footerSuite) => { + footerSuite + .setCaptureElements('.ascribe-footer') + .capture('footer', (actions, find) => { + actions.waitForElementToShow('.ascribe-footer', 5000); + }) + .capture('hover on footer item', (actions, find) => { + const footerItem = find('.ascribe-footer a:not(.social)'); + actions.mouseMove(footerItem); + }) + .capture('hover on footer social item', (actions, find) => { + const footerSocialItem = find('.ascribe-footer a.social') + actions.mouseMove(footerSocialItem); + }); + }); + + gemini.suite('Login', (loginSuite) => { + loginSuite + .capture('login', (actions, find) => { + actions.waitForElementToShow('.ascribe-form', 5000); + }) + .capture('hover on login submit', (actions, find) => { + actions.mouseMove(find('.ascribe-form button[type=submit]')); + }) + .capture('hover on sign up link', (actions, find) => { + actions.mouseMove(find('.ascribe-login-text a[href="/signup"]')); + }) + .capture('login form filled with focus', (actions, find) => { + const emailInput = find('.ascribe-form input[name=email]'); + + // Remove hover from sign up link + actions.click(emailInput); + + actions.sendKeys(emailInput, 'dimi@mailinator.com'); + actions.sendKeys(find('.ascribe-form input[name=password]'), '0000000000'); + }) + .capture('login form filled', (actions, find) => { + actions.click(find('.ascribe-form-header')); + }); + }); + + gemini.suite('Sign up', (signUpSuite) => { + signUpSuite + .setUrl('/signup') + .capture('sign up', (actions, find) => { + actions.waitForElementToShow('.ascribe-form', 5000); + }) + .capture('sign up form filled with focus', (actions, find) => { + actions.sendKeys(find('.ascribe-form input[name=email]'), 'dimi@mailinator.com'); + actions.sendKeys(find('.ascribe-form input[name=password]'), '0000000000'); + actions.sendKeys(find('.ascribe-form input[name=password_confirm]'), '0000000000'); + }) + .capture('sign up form filled with check', (actions, find) => { + actions.click(find('.ascribe-form input[type="checkbox"] ~ .checkbox')); + }); + }); + + gemini.suite('Password reset', (passwordResetSuite) => { + passwordResetSuite + .setUrl('/password_reset') + .capture('password reset', (actions, find) => { + actions.waitForElementToShow('.ascribe-form', 5000); + }) + .capture('password reset form filled with focus', (actions, find) => { + actions.sendKeys(find('.ascribe-form input[name="email"]'), 'dimi@mailinator.com'); + }) + .capture('password reset form filled', (actions, find) => { + actions.click(find('.ascribe-form-header')); + }); + }); + + gemini.suite('Coa verify', (coaVerifySuite) => { + coaVerifySuite + .setUrl('/coa_verify') + .capture('coa verify', (actions, find) => { + actions.waitForElementToShow('.ascribe-form', 5000); + }) + .capture('coa verify form filled with focus', (actions, find) => { + actions.sendKeys(find('.ascribe-form input[name="message"]'), 'sample text'); + actions.sendKeys(find('.ascribe-form .ascribe-property-wrapper:nth-of-type(2) textarea'), 'sample signature'); + }) + .capture('coa verify form filled', (actions, find) => { + actions.click(find('.ascribe-login-header')); + }); + }); + + gemini.suite('Not found', (notFoundSuite) => { + notFoundSuite + .setUrl('/not_found_page') + .capture('not found page'); + }); +}); diff --git a/gemini/main/detail.js b/gemini/main/detail.js new file mode 100644 index 00000000..39a02338 --- /dev/null +++ b/gemini/main/detail.js @@ -0,0 +1,125 @@ +'use strict'; + +const gemini = require('gemini'); +const pieceUrl = '/pieces/12374'; +const editionUrl = '/editions/14gw9x3VA9oJaxp4cHaAuK2bvJzvEj4Xvc'; + +/** + * Suite of tests against the piece and edition routes. + * Tests include accessing the piece / edition as the owner or as another user + * (we can just use an anonymous user in this case). +*/ +gemini.suite('Work detail', (suite) => { + suite + .setCaptureElements('.ascribe-body') + .before((actions, find) => { + // This will be called before every nested suite begins unless that suite + // also defines a `.before()` + // FIXME: use a more generic class for this, like just '.app', + // when we can use this file with the whitelabels + actions.waitForElementToShow('.ascribe-default-app', 5000); + + // Wait for the social media buttons to appear + actions.waitForElementToShow('.ascribe-social-button-list .fb-share-button iframe', 20000); + actions.waitForElementToShow('.ascribe-social-button-list .twitter-share-button', 20000); + actions.waitForElementToShow('.ascribe-media-player', 10000); + }); + + gemini.suite('Basic piece', (basicPieceSuite) => { + basicPieceSuite + .setUrl(pieceUrl) + .capture('basic piece') + .capture('shmui', (actions, find) => { + actions.click(find('.ascribe-media-player')); + actions.waitForElementToShow('.shmui-wrap:not(.loading)', 30000); + // Wait for the transition to end + actions.wait(1000); + }); + }); + + gemini.suite('Basic edition', (basicEditionSuite) => { + basicEditionSuite + .setUrl(editionUrl) + .capture('basic edition'); + }); + + // Suite just to log us in before any other suites run + gemini.suite('Login', (loginSuite) => { + loginSuite + .setUrl('/login') + .ignoreElements('.ascribe-body') + .before((actions, find) => { + actions.waitForElementToShow('.ascribe-default-app', 5000); + }) + .capture('logged in', (actions, find) => { + actions.sendKeys(find('.ascribe-login-wrapper input[name=email]'), 'dimi@mailinator.com'); + actions.sendKeys(find('.ascribe-login-wrapper input[name=password]'), '0000000000'); + actions.click(find('.ascribe-login-wrapper button[type=submit]')); + + actions.waitForElementToShow('.ascribe-accordion-list:not(.ascribe-loading-position)', 5000); + }); + }); + + gemini.suite('Authorized piece', (authorizedPieceSuite) => { + authorizedPieceSuite + .setUrl(pieceUrl) + .capture('authorized piece'); + }); + + gemini.suite('Authorized edition', (authorizedEditionSuite) => { + authorizedEditionSuite + .setUrl(editionUrl) + .capture('authorized edition') + }); + + gemini.suite('Detail action buttons', (detailActionButtonSuite) => { + detailActionButtonSuite + .setUrl(editionUrl) + .capture('hover on action button', (actions, find) => { + actions.mouseMove(find('.ascribe-detail-property .ascribe-button-list button.btn-default')); + }) + .capture('hover on delete button', (actions, find) => { + actions.mouseMove(find('.ascribe-detail-property .ascribe-button-list button.btn-tertiary')); + }) + .capture('hover on info button', (actions, find) => { + actions.mouseMove(find('.ascribe-detail-property .ascribe-button-list button.glyphicon-question-sign')); + }) + .capture('expand info text', (actions, find) => { + actions.click(find('.ascribe-detail-property .ascribe-button-list button.glyphicon-question-sign')); + }); + }); + + gemini.suite('Action form modal', (actionFormModalSuite) => { + actionFormModalSuite + .setUrl(editionUrl) + .setCaptureElements('.modal-dialog') + .capture('open email form', (actions, find) => { + // Add class names to make the action buttons easier to select + actions.executeJS(function (window) { + var actionButtons = window.document.querySelectorAll('.ascribe-detail-property .ascribe-button-list button.btn-default'); + for (var ii = 0; ii < actionButtons.length; ++ii) { + if (actionButtons[ii].textContent) { + actionButtons[ii].className += ' ascribe-action-button-' + actionButtons[ii].textContent.toLowerCase(); + } + } + }); + actions.click(find('.ascribe-detail-property .ascribe-button-list button.ascribe-action-button-email')); + + // Wait for transition + actions.wait(1000); + }); + }); + + // Suite just to log out after suites have run + gemini.suite('Log out', (logoutSuite) => { + logoutSuite + .setUrl('/logout') + .ignoreElements('.ascribe-body') + .before((actions, find) => { + actions.waitForElementToShow('.ascribe-default-app', 5000); + }) + .capture('logout', (actions, find) => { + actions.waitForElementToShow('.ascribe-login-wrapper', 10000); + }); + }); +}); From cf0a66ef2e792e4ffaee42d06467895f76275f60 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Fri, 29 Jan 2016 11:24:01 +0100 Subject: [PATCH 44/94] Add whitelabel visual test suite Currently just checks landing page and some pages accessible to unauthorized users. --- .gemini.yml | 116 +++++++++++++++++++ gemini/whitelabel/23vivi/23vivi.js | 27 +++++ gemini/whitelabel/cyland/cyland.js | 28 +++++ gemini/whitelabel/ikonotv/ikonotv.js | 95 +++++++++++++++ gemini/whitelabel/lumenus/lumenus.js | 27 +++++ gemini/whitelabel/shared/whitelabel_basic.js | 112 ++++++++++++++++++ 6 files changed, 405 insertions(+) create mode 100644 gemini/whitelabel/23vivi/23vivi.js create mode 100644 gemini/whitelabel/cyland/cyland.js create mode 100644 gemini/whitelabel/ikonotv/ikonotv.js create mode 100644 gemini/whitelabel/lumenus/lumenus.js create mode 100644 gemini/whitelabel/shared/whitelabel_basic.js diff --git a/.gemini.yml b/.gemini.yml index 8d434624..a04903eb 100644 --- a/.gemini.yml +++ b/.gemini.yml @@ -15,3 +15,119 @@ browsers: windowSize: 600x1056 desiredCapabilities: browserName: phantomjs + + CcDesktop: + rootUrl: http://cc.localhost.com:3000/ + screenshotsDir: './gemini-screens/cc-desktop' + windowSize: 1900x1080 + desiredCapabilities: + browserName: phantomjs + + CcMobile: + rootUrl: http://cc.localhost.com:3000/ + screenshotsDir: './gemini-screens/cc-mobile' + windowSize: 600x1056 + desiredCapabilities: + browserName: phantomjs + + CylandDesktop: + rootUrl: http://cyland.localhost.com:3000/ + screenshotsDir: './gemini-screens/cyland-desktop' + windowSize: 1900x1080 + desiredCapabilities: + browserName: phantomjs + + CylandMobile: + rootUrl: http://cyland.localhost.com:3000/ + screenshotsDir: './gemini-screens/cyland-mobile' + windowSize: 600x1056 + desiredCapabilities: + browserName: phantomjs + + IkonotvDesktop: + rootUrl: http://ikonotv.localhost.com:3000/ + screenshotsDir: './gemini-screens/ikonotv-desktop' + windowSize: 1900x1080 + desiredCapabilities: + browserName: phantomjs + + IkonotvMobile: + rootUrl: http://ikonotv.localhost.com:3000/ + screenshotsDir: './gemini-screens/ikonotv-mobile' + windowSize: 600x1056 + desiredCapabilities: + browserName: phantomjs + + LumenusDesktop: + rootUrl: http://lumenus.localhost.com:3000/ + screenshotsDir: './gemini-screens/lumenus-desktop' + windowSize: 1900x1080 + desiredCapabilities: + browserName: phantomjs + + LumenusMobile: + rootUrl: http://lumenus.localhost.com:3000/ + screenshotsDir: './gemini-screens/lumenus-mobile' + windowSize: 600x1056 + desiredCapabilities: + browserName: phantomjs + + 23viviDesktop: + rootUrl: http://23vivi.localhost.com:3000/ + screenshotsDir: './gemini-screens/23vivi-desktop' + windowSize: 1900x1080 + desiredCapabilities: + browserName: phantomjs + + 23viviMobile: + rootUrl: http://23vivi.localhost.com:3000/ + screenshotsDir: './gemini-screens/23vivi-mobile' + windowSize: 600x1056 + desiredCapabilities: + browserName: phantomjs + +sets: + main: + files: + - gemini/main + browsers: + - MainDesktop + - MainMobile + cc: + files: + - gemini/whitelabel/shared + browsers: + - CcDesktop + - CcMobile + + cyland: + files: + - gemini/whitelabel/shared + - gemini/whitelabel/cyland + browsers: + - CylandDesktop + - CylandMobile + + ikonotv: + files: + - gemini/whitelabel/shared + - gemini/whitelabel/ikonotv + browsers: + - IkonotvDesktop + - IkonotvMobile + + lumenus: + files: + - gemini/whitelabel/shared + - gemini/whitelabel/lumenus + browsers: + - LumenusDesktop + - LumenusMobile + + 23vivi: + files: + - gemini/whitelabel/shared + - gemini/whitelabel/23vivi + browsers: + - 23viviDesktop + - 23viviMobile diff --git a/gemini/whitelabel/23vivi/23vivi.js b/gemini/whitelabel/23vivi/23vivi.js new file mode 100644 index 00000000..cafdfc6d --- /dev/null +++ b/gemini/whitelabel/23vivi/23vivi.js @@ -0,0 +1,27 @@ +'use strict'; + +const gemini = require('gemini'); + +/** + * Suite of tests against 23vivi specific routes + */ +gemini.suite('23vivi', (suite) => { + suite + //TODO: maybe this should be changed to .ascribe-body once the PR that does this is merged + .setCaptureElements('.ascribe-wallet-app') + .before((actions, find) => { + // This will be called before every nested suite begins + actions.waitForElementToShow('.ascribe-wallet-app', 5000); + }); + + gemini.suite('Landing', (landingSuite) => { + landingSuite + .setUrl('/') + .capture('landing', (actions, find) => { + // Wait for the logo to appear + actions.waitForElementToShow('.vivi23-landing--header-logo', 10000); + }); + }); + + // TODO: add more tests for market specific pages after authentication +}); diff --git a/gemini/whitelabel/cyland/cyland.js b/gemini/whitelabel/cyland/cyland.js new file mode 100644 index 00000000..06709f39 --- /dev/null +++ b/gemini/whitelabel/cyland/cyland.js @@ -0,0 +1,28 @@ +'use strict'; + +const gemini = require('gemini'); + +/** + * Suite of tests against Cyland specific routes + */ +gemini.suite('Cyland', (suite) => { + suite + //TODO: maybe this should be changed to .ascribe-body once the PR that does this is merged + .setCaptureElements('.ascribe-wallet-app') + .before((actions, find) => { + // This will be called before every nested suite begins + actions.waitForElementToShow('.ascribe-wallet-app', 5000); + }); + + gemini.suite('Landing', (landingSuite) => { + landingSuite + .setUrl('/') + // Ignore Cyland's logo as it's a gif + .ignoreElements('.cyland-landing img') + .capture('landing', (actions, find) => { + actions.waitForElementToShow('.cyland-landing img', 10000); + }); + }); + + // TODO: add more tests for cyland specific pages after authentication +}); diff --git a/gemini/whitelabel/ikonotv/ikonotv.js b/gemini/whitelabel/ikonotv/ikonotv.js new file mode 100644 index 00000000..1741aaa0 --- /dev/null +++ b/gemini/whitelabel/ikonotv/ikonotv.js @@ -0,0 +1,95 @@ +'use strict'; + +const gemini = require('gemini'); + +/** + * Suite of tests against Cyland specific routes + */ +gemini.suite('Ikonotv', (suite) => { + suite + //TODO: maybe this should be changed to .ascribe-body once the PR that does this is merged + .setCaptureElements('.ascribe-wallet-app') + .before((actions, find) => { + // This will be called before every nested suite begins + actions.waitForElementToShow('.ascribe-wallet-app', 5000); + }); + + gemini.suite('Landing', (landingSuite) => { + landingSuite + .setUrl('/') + // Gemini complains if we try to capture the entire app for Ikonotv's landing page for some reason + .setCaptureElements('.ikonotv-landing') + .setTolerance(5) + .capture('landing', (actions, find) => { + // Stop background animation + actions.executeJS(function (window) { + var landingBackground = window.document.querySelector('.client--ikonotv .route--landing'); + landingBackground.style.animation = 'none'; + landingBackground.style.webkitAnimation = 'none'; + }); + + // Wait for logo to appear + actions.waitForElementToShow('.ikonotv-landing header img', 10000); + }); + }); + + // Ikono needs its own set of tests for some pre-authorization pages to wait for + // its logo to appear + gemini.suite('Ikonotv basic', (suite) => { + suite + .setCaptureElements('.ascribe-wallet-app') + .before((actions, find) => { + // This will be called before every nested suite begins unless that suite + // also defines a `.before()` + // FIXME: use a more generic class for this, like just '.app', + // when we can use this file with the whitelabels + actions.waitForElementToShow('.ascribe-wallet-app', 5000); + + // Wait for the forms to appear + actions.waitForElementToShow('.ascribe-form', 5000); + + // Just use a dumb wait because the logo is set as a background image + actions.wait(3000); + }); + + gemini.suite('Login', (loginSuite) => { + loginSuite + .setUrl('/login') + .capture('login') + .capture('hover on login submit', (actions, find) => { + actions.mouseMove(find('.ascribe-form button[type=submit]')); + }) + .capture('hover on sign up link', (actions, find) => { + actions.mouseMove(find('.ascribe-login-text a[href="/signup"]')); + }) + .capture('login form filled with focus', (actions, find) => { + const emailInput = find('.ascribe-form input[name=email]'); + + // Remove hover from sign up link + actions.click(emailInput); + + actions.sendKeys(emailInput, 'dimi@mailinator.com'); + actions.sendKeys(find('.ascribe-form input[name=password]'), '0000000000'); + }) + .capture('login form filled', (actions, find) => { + actions.click(find('.ascribe-form-header')); + }); + }); + + gemini.suite('Sign up', (signUpSuite) => { + signUpSuite + .setUrl('/signup') + .capture('sign up') + .capture('sign up form filled with focus', (actions, find) => { + actions.sendKeys(find('.ascribe-form input[name=email]'), 'dimi@mailinator.com'); + actions.sendKeys(find('.ascribe-form input[name=password]'), '0000000000'); + actions.sendKeys(find('.ascribe-form input[name=password_confirm]'), '0000000000'); + }) + .capture('sign up form filled with check', (actions, find) => { + actions.click(find('.ascribe-form input[type="checkbox"] ~ .checkbox')); + }); + }); + }); + + // TODO: add more tests for ikonotv specific pages after authentication +}); diff --git a/gemini/whitelabel/lumenus/lumenus.js b/gemini/whitelabel/lumenus/lumenus.js new file mode 100644 index 00000000..a9ff53cd --- /dev/null +++ b/gemini/whitelabel/lumenus/lumenus.js @@ -0,0 +1,27 @@ +'use strict'; + +const gemini = require('gemini'); + +/** + * Suite of tests against lumenus specific routes + */ +gemini.suite('Lumenus', (suite) => { + suite + //TODO: maybe this should be changed to .ascribe-body once the PR that does this is merged + .setCaptureElements('.ascribe-wallet-app') + .before((actions, find) => { + // This will be called before every nested suite begins + actions.waitForElementToShow('.ascribe-wallet-app', 5000); + }); + + gemini.suite('Landing', (landingSuite) => { + landingSuite + .setUrl('/') + .capture('landing', (actions, find) => { + // Wait for the logo to appear + actions.waitForElementToShow('.wp-landing-wrapper img', 10000); + }); + }); + + // TODO: add more tests for market specific pages after authentication +}); diff --git a/gemini/whitelabel/shared/whitelabel_basic.js b/gemini/whitelabel/shared/whitelabel_basic.js new file mode 100644 index 00000000..7fe5c256 --- /dev/null +++ b/gemini/whitelabel/shared/whitelabel_basic.js @@ -0,0 +1,112 @@ +'use strict'; + +const gemini = require('gemini'); + +/** + * Basic suite of tests against whitelabel routes that do not require authentication. +*/ +gemini.suite('Whitelabel basic', (suite) => { + suite + .setCaptureElements('.ascribe-wallet-app > .container') + .before((actions, find) => { + // This will be called before every nested suite begins unless that suite + // also defines a `.before()` + // FIXME: use a more generic class for this, like just '.ascribe-app' + actions.waitForElementToShow('.ascribe-wallet-app', 5000); + + // Use a dumb wait in case we're still waiting for other assets, like fonts, to load + actions.wait(1000); + }); + + gemini.suite('Login', (loginSuite) => { + loginSuite + .setUrl('/login') + // See Ikono + .skip(/Ikono/) + .capture('login', (actions, find) => { + actions.waitForElementToShow('.ascribe-form', 5000); + // For some reason, the screenshots seem to keep catching the whitelabel login form + // on a refresh and without fonts loaded (maybe because they're the first tests run + // and the cache isn't hot yet?). + // Let's wait a bit and hope they load. + actions.wait(3000); + }) + .capture('hover on login submit', (actions, find) => { + actions.mouseMove(find('.ascribe-form button[type=submit]')); + }) + .capture('hover on sign up link', (actions, find) => { + actions.mouseMove(find('.ascribe-login-text a[href="/signup"]')); + }) + .capture('login form filled with focus', (actions, find) => { + const emailInput = find('.ascribe-form input[name=email]'); + + // Remove hover from sign up link + actions.click(emailInput); + + actions.sendKeys(emailInput, 'dimi@mailinator.com'); + actions.sendKeys(find('.ascribe-form input[name=password]'), '0000000000'); + }) + .capture('login form filled', (actions, find) => { + actions.click(find('.ascribe-form-header')); + }); + }); + + gemini.suite('Sign up', (signUpSuite) => { + signUpSuite + .setUrl('/signup') + // See Ikono + .skip(/Ikono/) + .capture('sign up', (actions, find) => { + actions.waitForElementToShow('.ascribe-form', 5000); + // Wait in case the form reloads due to other assets loading + actions.wait(500); + }) + .capture('sign up form filled with focus', (actions, find) => { + actions.sendKeys(find('.ascribe-form input[name=email]'), 'dimi@mailinator.com'); + actions.sendKeys(find('.ascribe-form input[name=password]'), '0000000000'); + actions.sendKeys(find('.ascribe-form input[name=password_confirm]'), '0000000000'); + }) + .capture('sign up form filled with check', (actions, find) => { + actions.click(find('.ascribe-form input[type="checkbox"] ~ .checkbox')); + }); + }); + + gemini.suite('Password reset', (passwordResetSuite) => { + passwordResetSuite + .setUrl('/password_reset') + .capture('password reset', (actions, find) => { + actions.waitForElementToShow('.ascribe-form', 5000); + // Wait in case the form reloads due to other assets loading + actions.wait(500); + }) + .capture('password reset form filled with focus', (actions, find) => { + actions.sendKeys(find('.ascribe-form input[name="email"]'), 'dimi@mailinator.com'); + }) + .capture('password reset form filled', (actions, find) => { + actions.click(find('.ascribe-form-header')); + }); + }); + + gemini.suite('Coa verify', (coaVerifySuite) => { + coaVerifySuite + .setUrl('/coa_verify') + .capture('coa verify', (actions, find) => { + actions.waitForElementToShow('.ascribe-form', 5000); + // Wait in case the form reloads due to other assets loading + actions.wait(500); + }) + .capture('coa verify form filled with focus', (actions, find) => { + actions.sendKeys(find('.ascribe-form input[name="message"]'), 'sample text'); + actions.sendKeys(find('.ascribe-form .ascribe-property-wrapper:nth-of-type(2) textarea'), 'sample signature'); + }) + .capture('coa verify form filled', (actions, find) => { + actions.click(find('.ascribe-login-header')); + }); + }); + + gemini.suite('Not found', (notFoundSuite) => { + notFoundSuite + .setUrl('/not_found_page') + .capture('not found page'); + }); +}); From 331fdb528de81d2a3378502473f6e3793b035cb7 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Fri, 29 Jan 2016 11:24:28 +0100 Subject: [PATCH 45/94] Remove browser-sync notify pop over that shows up in visual tests --- gulpfile.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gulpfile.js b/gulpfile.js index f13945b0..afa0d5a9 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -97,7 +97,8 @@ gulp.task('browser-sync', function() { proxy: 'http://localhost:4000', port: 3000, open: false, // does not open the browser-window anymore (handled manually) - ghostMode: false + ghostMode: false, + notify: false // stop showing the browsersync pop up }); }); From 83b5b09b874155522b45734581c50b32fe3da6c9 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Fri, 29 Jan 2016 11:26:10 +0100 Subject: [PATCH 46/94] Add npm scripts for running visual tests --- docs/visual-regression-testing.md | 33 +++++++++++++++++++++++++------ package.json | 17 ++++++++++++++-- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/docs/visual-regression-testing.md b/docs/visual-regression-testing.md index d544b1fa..35d328f0 100644 --- a/docs/visual-regression-testing.md +++ b/docs/visual-regression-testing.md @@ -42,21 +42,32 @@ Running Tests Run PhantomJS: ```bash -phantomjs --webdriver=4444 +npm run vi-phantom ``` And then run Gemini tests: ```bash -# In root onion/ -gemini test gemini/* --report html +npm run vi-test + +# Run only main tests +npm run vi-test:main + +# Run only whitelabel tests +npm run vi-test:whitelabel + +# Run only specific whitelabel tests +npm run vi-test:cyland ``` -If you've made changes and want them to be the new baseline (ie. it's a correct change--**make sure** to test there are no regressions first!), use +If you've made changes and want them to be the new baseline (ie. it's a correct change--**make sure** to test there are +no regressions first!), use ```bash -# In root onion/ -gemini update gemini/* +npm run vi-update + +# Update just the main app for desktop and mobile +npm run vi-update -- --browser MainDesktop --browser MainMobile ``` @@ -112,6 +123,8 @@ are available. Our tests are located in `onion/gemini/`. +**It would be nice if we kept the whitelabels up to date.** + Some useful tips: * The `find()` method in the callbacks is equivalent to `document.querySelector`; it will only return the first element found that matches the selector. Use pseudo classes like `nth-of-type()`, `nth-child()`, and etc. to select @@ -181,3 +194,11 @@ change the environment to run against. # In root /onion folder phantomjs phantomjs/launch_app_and_login.js ``` + + +TODO +==== + +* Write scripts to automate creation of test users (and modify tests to accomodate) +* Set scripts with rootUrls pointing to staging / live using environment variables +* Set up with Sauce Labs diff --git a/package.json b/package.json index d3e7ae41..dd8b3244 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,20 @@ "lint": "eslint ./js", "postinstall": "npm run build", "build": "gulp build --production", - "start": "node server.js" + "start": "node server.js", + + "vi-clean": "rm -rf gemini-report", + "vi-phantom": "phantomjs --webdriver=4444", + "vi-update": "gemini update", + "vi-test": "npm run vi-clean && gemini test --reporter html --reporter vflat || true", + "vi-test:all": "npm run vi-test", + "vi-test:main": "npm run vi-test -- --browser MainDesktop --browser MainMobile", + "vi-test:whitelabel": "GEMINI_BROWSERS='CcDesktop, CcMobile, CylandDesktop, CylandMobile, IkonotvDesktop, IkonotvMobile, LumenusDesktop, LumenusMobile, 23viviDesktop, 23viviMobile' npm run vi-test", + "vi-test:cc": "npm run vi-test -- --browser CcDesktop --browser CcMobile", + "vi-test:cyland": "npm run vi-test -- --browser CylandDesktop --browser CylandMobile", + "vi-test:ikonotv": "npm run vi-test -- --browser IkonotvDesktop --browser IkonotvMobile", + "vi-test:lumenus": "npm run vi-test -- --browser LumenusDesktop --browser LumenusMobile", + "vi-test:23vivi": "npm run vi-test -- --browser 23viviDesktop --browser 23viviMobile" }, "browser": { "fineUploader": "./js/components/ascribe_uploader/vendor/s3.fine-uploader.js" @@ -35,7 +48,7 @@ "devDependencies": { "babel-eslint": "^3.1.11", "babel-jest": "^5.2.0", - "gemini": "^2.0.3", + "gemini": "^2.1.0", "gulp-sass": "^2.1.1", "jest-cli": "^0.4.0", "phantomjs2": "^2.0.2" From cc072199a12fca547d5ed295ab44d8e764e0061f Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Fri, 29 Jan 2016 11:48:37 +0100 Subject: [PATCH 47/94] Update npm scripts for sauce labs --- package.json | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index ba9ba324..bfe0c416 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,9 @@ "postinstall": "npm run build", "build": "gulp build --production", "start": "node server.js", - - "test": "mocha", - "tunnel": "node test/tunnel.js" - + "test": "npm run sauce-test", + "sauce-test": "mocha", + "tunnel": "node test/tunnel.js", "vi-clean": "rm -rf gemini-report", "vi-phantom": "phantomjs --webdriver=4444", "vi-update": "gemini update", From 8cb953618729eaf73315045b866bf75c7939b01b Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Fri, 29 Jan 2016 13:39:46 +0100 Subject: [PATCH 48/94] Move sauce tests to /test/integration folder --- package.json | 4 ++-- test/{ => integration}/README.md | 0 test/{ => integration}/config.js | 0 test/{ => integration}/setup.js | 0 test/{ => integration/tests}/test-login.js | 2 +- test/{ => integration}/tunnel.js | 0 6 files changed, 3 insertions(+), 3 deletions(-) rename test/{ => integration}/README.md (100%) rename test/{ => integration}/config.js (100%) rename test/{ => integration}/setup.js (100%) rename test/{ => integration/tests}/test-login.js (97%) rename test/{ => integration}/tunnel.js (100%) diff --git a/package.json b/package.json index bfe0c416..42c8bba9 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,8 @@ "build": "gulp build --production", "start": "node server.js", "test": "npm run sauce-test", - "sauce-test": "mocha", - "tunnel": "node test/tunnel.js", + "sauce-test": "mocha ./test/integration/tests/", + "tunnel": "node ./test/integration/tunnel.js", "vi-clean": "rm -rf gemini-report", "vi-phantom": "phantomjs --webdriver=4444", "vi-update": "gemini update", diff --git a/test/README.md b/test/integration/README.md similarity index 100% rename from test/README.md rename to test/integration/README.md diff --git a/test/config.js b/test/integration/config.js similarity index 100% rename from test/config.js rename to test/integration/config.js diff --git a/test/setup.js b/test/integration/setup.js similarity index 100% rename from test/setup.js rename to test/integration/setup.js diff --git a/test/test-login.js b/test/integration/tests/test-login.js similarity index 97% rename from test/test-login.js rename to test/integration/tests/test-login.js index e2736fe1..853d48e5 100644 --- a/test/test-login.js +++ b/test/integration/tests/test-login.js @@ -5,7 +5,7 @@ const wd = require('wd'); const asserters = wd.asserters; // Commonly used asserters for async waits in the browser const chai = require('chai'); const chaiAsPromised = require('chai-as-promised'); -const config = require('./config.js'); +const config = require('../config.js'); chai.use(chaiAsPromised); chai.should(); diff --git a/test/tunnel.js b/test/integration/tunnel.js similarity index 100% rename from test/tunnel.js rename to test/integration/tunnel.js From 27ebf9d71138733f8472c8f0bde171168aae538b Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Fri, 29 Jan 2016 13:58:52 +0100 Subject: [PATCH 49/94] Move visual tests to /test/gemini folder --- package.json | 6 +++--- .gemini.yml => test/gemini/.gemini.yml | 20 +++++++++---------- .../gemini/README.md | 3 ++- .../gemini/tests}/main/authenticated.js | 0 {gemini => test/gemini/tests}/main/basic.js | 0 {gemini => test/gemini/tests}/main/detail.js | 0 .../gemini/tests}/whitelabel/23vivi/23vivi.js | 0 .../gemini/tests}/whitelabel/cyland/cyland.js | 0 .../tests}/whitelabel/ikonotv/ikonotv.js | 0 .../tests}/whitelabel/lumenus/lumenus.js | 0 .../whitelabel/shared/whitelabel_basic.js | 0 11 files changed, 15 insertions(+), 14 deletions(-) rename .gemini.yml => test/gemini/.gemini.yml (89%) rename docs/visual-regression-testing.md => test/gemini/README.md (97%) rename {gemini => test/gemini/tests}/main/authenticated.js (100%) rename {gemini => test/gemini/tests}/main/basic.js (100%) rename {gemini => test/gemini/tests}/main/detail.js (100%) rename {gemini => test/gemini/tests}/whitelabel/23vivi/23vivi.js (100%) rename {gemini => test/gemini/tests}/whitelabel/cyland/cyland.js (100%) rename {gemini => test/gemini/tests}/whitelabel/ikonotv/ikonotv.js (100%) rename {gemini => test/gemini/tests}/whitelabel/lumenus/lumenus.js (100%) rename {gemini => test/gemini/tests}/whitelabel/shared/whitelabel_basic.js (100%) diff --git a/package.json b/package.json index 42c8bba9..5e72b863 100644 --- a/package.json +++ b/package.json @@ -15,10 +15,10 @@ "test": "npm run sauce-test", "sauce-test": "mocha ./test/integration/tests/", "tunnel": "node ./test/integration/tunnel.js", - "vi-clean": "rm -rf gemini-report", + "vi-clean": "rm -rf ./gemini-report", "vi-phantom": "phantomjs --webdriver=4444", - "vi-update": "gemini update", - "vi-test": "npm run vi-clean && gemini test --reporter html --reporter vflat || true", + "vi-update": "gemini update -c ./test/gemini/.gemini.yml", + "vi-test": "npm run vi-clean && gemini test -c ./test/gemini/.gemini.yml --reporter html --reporter vflat || true", "vi-test:all": "npm run vi-test", "vi-test:main": "npm run vi-test -- --browser MainDesktop --browser MainMobile", "vi-test:whitelabel": "GEMINI_BROWSERS='CcDesktop, CcMobile, CylandDesktop, CylandMobile, IkonotvDesktop, IkonotvMobile, LumenusDesktop, LumenusMobile, 23viviDesktop, 23viviMobile' npm run vi-test", diff --git a/.gemini.yml b/test/gemini/.gemini.yml similarity index 89% rename from .gemini.yml rename to test/gemini/.gemini.yml index a04903eb..f64d865f 100644 --- a/.gemini.yml +++ b/test/gemini/.gemini.yml @@ -89,45 +89,45 @@ browsers: sets: main: files: - - gemini/main + - tests/main browsers: - MainDesktop - MainMobile cc: files: - - gemini/whitelabel/shared + - tests/whitelabel/shared browsers: - CcDesktop - CcMobile cyland: files: - - gemini/whitelabel/shared - - gemini/whitelabel/cyland + - tests/whitelabel/shared + - tests/whitelabel/cyland browsers: - CylandDesktop - CylandMobile ikonotv: files: - - gemini/whitelabel/shared - - gemini/whitelabel/ikonotv + - tests/whitelabel/shared + - tests/whitelabel/ikonotv browsers: - IkonotvDesktop - IkonotvMobile lumenus: files: - - gemini/whitelabel/shared - - gemini/whitelabel/lumenus + - tests/whitelabel/shared + - tests/whitelabel/lumenus browsers: - LumenusDesktop - LumenusMobile 23vivi: files: - - gemini/whitelabel/shared - - gemini/whitelabel/23vivi + - tests/whitelabel/shared + - tests/whitelabel/23vivi browsers: - 23viviDesktop - 23viviMobile diff --git a/docs/visual-regression-testing.md b/test/gemini/README.md similarity index 97% rename from docs/visual-regression-testing.md rename to test/gemini/README.md index 35d328f0..da1712d9 100644 --- a/docs/visual-regression-testing.md +++ b/test/gemini/README.md @@ -24,6 +24,7 @@ Then, install [PhantomJS2](https://www.npmjs.com/package/phantomjs2): ```bash # Until phantomjs2 is updated for the new 2.1 version of PhantomJS, use the following (go to https://bitbucket.org/ariya/phantomjs/downloads to find a build for your OS) npm install -g phantomjs2 --phantomjs_downloadurl=https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-macosx.zip +npm install --save-dev phantomjs2 --phantomjs_downloadurl=https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-macosx.zip # If using OSX, you may have to install upx and decompress the binary downloaded by npm manually: brew install upx @@ -121,7 +122,7 @@ See [the docs](https://github.com/gemini-testing/gemini/blob/master/doc/tests.md actions](https://github.com/gemini-testing/gemini/blob/master/doc/tests.md#available-actions) for what scripted actions are available. -Our tests are located in `onion/gemini/`. +Our tests are located in `onion/test/gemini/tests/`. **It would be nice if we kept the whitelabels up to date.** diff --git a/gemini/main/authenticated.js b/test/gemini/tests/main/authenticated.js similarity index 100% rename from gemini/main/authenticated.js rename to test/gemini/tests/main/authenticated.js diff --git a/gemini/main/basic.js b/test/gemini/tests/main/basic.js similarity index 100% rename from gemini/main/basic.js rename to test/gemini/tests/main/basic.js diff --git a/gemini/main/detail.js b/test/gemini/tests/main/detail.js similarity index 100% rename from gemini/main/detail.js rename to test/gemini/tests/main/detail.js diff --git a/gemini/whitelabel/23vivi/23vivi.js b/test/gemini/tests/whitelabel/23vivi/23vivi.js similarity index 100% rename from gemini/whitelabel/23vivi/23vivi.js rename to test/gemini/tests/whitelabel/23vivi/23vivi.js diff --git a/gemini/whitelabel/cyland/cyland.js b/test/gemini/tests/whitelabel/cyland/cyland.js similarity index 100% rename from gemini/whitelabel/cyland/cyland.js rename to test/gemini/tests/whitelabel/cyland/cyland.js diff --git a/gemini/whitelabel/ikonotv/ikonotv.js b/test/gemini/tests/whitelabel/ikonotv/ikonotv.js similarity index 100% rename from gemini/whitelabel/ikonotv/ikonotv.js rename to test/gemini/tests/whitelabel/ikonotv/ikonotv.js diff --git a/gemini/whitelabel/lumenus/lumenus.js b/test/gemini/tests/whitelabel/lumenus/lumenus.js similarity index 100% rename from gemini/whitelabel/lumenus/lumenus.js rename to test/gemini/tests/whitelabel/lumenus/lumenus.js diff --git a/gemini/whitelabel/shared/whitelabel_basic.js b/test/gemini/tests/whitelabel/shared/whitelabel_basic.js similarity index 100% rename from gemini/whitelabel/shared/whitelabel_basic.js rename to test/gemini/tests/whitelabel/shared/whitelabel_basic.js From 29b1d8f7e5267cdb22a880a94f0f621e4eb1356e Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Fri, 29 Jan 2016 13:58:57 +0100 Subject: [PATCH 50/94] Update README for updated paths to tests --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1b918e5d..8dcf78ad 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,14 @@ Visual Regression Testing We're using [Gemini](https://github.com/gemini-testing/gemini) for visual regression tests because it supports both PhantomJS2 and SauceLabs. -See the [helper docs](docs/visual-regression-testing.md) for information on installing Gemini, its dependencies, and running and writing tests. +See the [helper docs](test/gemini/README.md) for information on installing Gemini, its dependencies, and running and writing tests. + +Integration Testing +------------------- + +We're using [Sauce Labs](https://saucelabs.com/home) with [WD.js](https://github.com/admc/wd) for integration testing across browser grids with Selenium. + +See the [helper docs](test/integration/README.md) for information on each part of the test stack and how to run and write tests. Workflow From 11f644dd117ec5ed550d7740daa5e8d7bae422b7 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Fri, 29 Jan 2016 14:37:41 +0100 Subject: [PATCH 51/94] Fix shmui visual test --- test/gemini/tests/main/detail.js | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/test/gemini/tests/main/detail.js b/test/gemini/tests/main/detail.js index 39a02338..7adad5d4 100644 --- a/test/gemini/tests/main/detail.js +++ b/test/gemini/tests/main/detail.js @@ -29,12 +29,17 @@ gemini.suite('Work detail', (suite) => { basicPieceSuite .setUrl(pieceUrl) .capture('basic piece') - .capture('shmui', (actions, find) => { - actions.click(find('.ascribe-media-player')); - actions.waitForElementToShow('.shmui-wrap:not(.loading)', 30000); - // Wait for the transition to end - actions.wait(1000); - }); + + gemini.suite('Shmui', (shmuiSuite) => { + shmuiSuite. + setCaptureElements('.shmui-wrap') + .capture('shmui', (actions, find) => { + actions.click(find('.ascribe-media-player')); + actions.waitForElementToShow('.shmui-wrap:not(.loading)', 30000); + // Wait for the transition to end + actions.wait(1000); + }); + }); }); gemini.suite('Basic edition', (basicEditionSuite) => { From a785fbc22047ec1a81cae6228638791837df5fb1 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Fri, 29 Jan 2016 14:37:55 +0100 Subject: [PATCH 52/94] Change screenshot folder for visual tests --- test/gemini/.gemini.yml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/test/gemini/.gemini.yml b/test/gemini/.gemini.yml index f64d865f..0c7b8b77 100644 --- a/test/gemini/.gemini.yml +++ b/test/gemini/.gemini.yml @@ -4,84 +4,84 @@ sessionsPerBrowser: 1 browsers: MainDesktop: rootUrl: http://localhost.com:3000/ - screenshotsDir: './gemini-screens/desktop' + screenshotsDir: './screenshots/main-desktop' windowSize: 1900x1080 desiredCapabilities: browserName: phantomjs MainMobile: rootUrl: http://localhost.com:3000/ - screenshotsDir: './gemini-screens/mobile' + screenshotsDir: './screenshots/main-mobile' windowSize: 600x1056 desiredCapabilities: browserName: phantomjs CcDesktop: rootUrl: http://cc.localhost.com:3000/ - screenshotsDir: './gemini-screens/cc-desktop' + screenshotsDir: './screenshots/cc-desktop' windowSize: 1900x1080 desiredCapabilities: browserName: phantomjs CcMobile: rootUrl: http://cc.localhost.com:3000/ - screenshotsDir: './gemini-screens/cc-mobile' + screenshotsDir: './screenshots/cc-mobile' windowSize: 600x1056 desiredCapabilities: browserName: phantomjs CylandDesktop: rootUrl: http://cyland.localhost.com:3000/ - screenshotsDir: './gemini-screens/cyland-desktop' + screenshotsDir: './screenshots/cyland-desktop' windowSize: 1900x1080 desiredCapabilities: browserName: phantomjs CylandMobile: rootUrl: http://cyland.localhost.com:3000/ - screenshotsDir: './gemini-screens/cyland-mobile' + screenshotsDir: './screenshots/cyland-mobile' windowSize: 600x1056 desiredCapabilities: browserName: phantomjs IkonotvDesktop: rootUrl: http://ikonotv.localhost.com:3000/ - screenshotsDir: './gemini-screens/ikonotv-desktop' + screenshotsDir: './screenshots/ikonotv-desktop' windowSize: 1900x1080 desiredCapabilities: browserName: phantomjs IkonotvMobile: rootUrl: http://ikonotv.localhost.com:3000/ - screenshotsDir: './gemini-screens/ikonotv-mobile' + screenshotsDir: './screenshots/ikonotv-mobile' windowSize: 600x1056 desiredCapabilities: browserName: phantomjs LumenusDesktop: rootUrl: http://lumenus.localhost.com:3000/ - screenshotsDir: './gemini-screens/lumenus-desktop' + screenshotsDir: './screenshots/lumenus-desktop' windowSize: 1900x1080 desiredCapabilities: browserName: phantomjs LumenusMobile: rootUrl: http://lumenus.localhost.com:3000/ - screenshotsDir: './gemini-screens/lumenus-mobile' + screenshotsDir: './screenshots/lumenus-mobile' windowSize: 600x1056 desiredCapabilities: browserName: phantomjs 23viviDesktop: rootUrl: http://23vivi.localhost.com:3000/ - screenshotsDir: './gemini-screens/23vivi-desktop' + screenshotsDir: './screenshots/23vivi-desktop' windowSize: 1900x1080 desiredCapabilities: browserName: phantomjs 23viviMobile: rootUrl: http://23vivi.localhost.com:3000/ - screenshotsDir: './gemini-screens/23vivi-mobile' + screenshotsDir: './screenshots/23vivi-mobile' windowSize: 600x1056 desiredCapabilities: browserName: phantomjs From f5a341b37e054de3b4ab3096c8a7630d4ba9ccad Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Mon, 1 Feb 2016 17:12:49 +0100 Subject: [PATCH 53/94] Add environment config file for visual regression tests Wayyyyy better than hard coding diminator everywhere. --- phantomjs/launch_app_and_login.js | 62 ---------------- test/gemini/README.md | 9 ++- test/gemini/tests/environment.js | 22 ++++++ test/gemini/tests/main/authenticated.js | 5 +- test/gemini/tests/main/basic.js | 13 ++-- test/gemini/tests/main/detail.js | 9 +-- .../tests/whitelabel/ikonotv/ikonotv.js | 11 +-- .../whitelabel/shared/whitelabel_basic.js | 13 ++-- test/phantomjs/launch_app_and_login.js | 71 +++++++++++++++++++ 9 files changed, 127 insertions(+), 88 deletions(-) delete mode 100644 phantomjs/launch_app_and_login.js create mode 100644 test/gemini/tests/environment.js create mode 100644 test/phantomjs/launch_app_and_login.js diff --git a/phantomjs/launch_app_and_login.js b/phantomjs/launch_app_and_login.js deleted file mode 100644 index e5418519..00000000 --- a/phantomjs/launch_app_and_login.js +++ /dev/null @@ -1,62 +0,0 @@ -'use strict'; - -var liveEnv = 'https://www.ascribe.io/app/login'; -// Note that if you are trying to access staging, you will need to use -// the --ignore-ssl-errors=true flag on phantomjs -var stagingEnv = 'https://www.ascribe.ninja/app/login'; -var localEnv = 'http://localhost.com:3000/login'; - -var page = require('webpage').create(); -page.open(localEnv, function(status) { - var attemptedToLogIn; - var loginCheckInterval; - - console.log('Status: ' + status); - - if (status === 'success') { - console.log('Attempting to log in...'); - - attemptedToLogIn = page.evaluate(function () { - try { - var inputForm = document.querySelector('.ascribe-login-wrapper'); - var email = inputForm.querySelector('input[type=email]'); - var password = inputForm.querySelector('input[type=password]'); - var submitBtn = inputForm.querySelector('button[type=submit]'); - - email.value = 'dimi@mailinator.com'; - password.value = '0000000000'; - submitBtn.click(); - - return true; - } catch (ex) { - console.log('Error while trying to find login elements, not logging in.'); - return false; - } - }); - - if (attemptedToLogIn) { - loginCheckInterval = setInterval(function () { - var loggedIn = page.evaluate(function () { - // When they log in, they are taken to the collections page. - // When the piece list is loaded, the accordion list is either available or - // shows a placeholder, so let's check for these elements to determine - // when login is finished - return !!(document.querySelector('.ascribe-accordion-list:not(.ascribe-loading-position)') || - document.querySelector('.ascribe-accordion-list-placeholder')); - }); - - if (loggedIn) { - clearInterval(loginCheckInterval); - console.log('Successfully logged in.'); - } - }, 1000); - } else { - console.log('Something happened while trying to log in, aborting...'); - phantom.exit(); - } - - } else { - console.log('Failed to load page, exiing...'); - phantom.exit(); - } -}); diff --git a/test/gemini/README.md b/test/gemini/README.md index da1712d9..bc029041 100644 --- a/test/gemini/README.md +++ b/test/gemini/README.md @@ -122,9 +122,12 @@ See [the docs](https://github.com/gemini-testing/gemini/blob/master/doc/tests.md actions](https://github.com/gemini-testing/gemini/blob/master/doc/tests.md#available-actions) for what scripted actions are available. -Our tests are located in `onion/test/gemini/tests/`. +Our tests are located in `onion/test/gemini/tests/`. For now, the tests use the environment defined in +`onion/test/gemini/tests/environment.js` for which user, piece, and edition to run tests against. In the future, it'd be +nice if we had some db scripts that we could use to populate a test db for these regression tests. -**It would be nice if we kept the whitelabels up to date.** +**It would also be nice if we kept the whitelabels up to date, so if you add one, please also test (at least) its landing +page.** Some useful tips: * The `find()` method in the callbacks is equivalent to `document.querySelector`; it will only return the first @@ -193,7 +196,7 @@ change the environment to run against. ```bash # In root /onion folder -phantomjs phantomjs/launch_app_and_login.js +phantomjs test/phantomjs/launch_app_and_login.js ``` diff --git a/test/gemini/tests/environment.js b/test/gemini/tests/environment.js new file mode 100644 index 00000000..4cb1dacf --- /dev/null +++ b/test/gemini/tests/environment.js @@ -0,0 +1,22 @@ +'use strict'; + +const mainUser = { + email: 'dimi@mailinator.com', + password: '0000000000' +}; +const mainPieceId = '12374'; +const mainEditionId = '14gw9x3VA9oJaxp4cHaAuK2bvJzvEj4Xvc'; + +console.log('================== Test environment ==================\n'); +console.log('Main user:'); +console.log(` Email: ${mainUser.email}`); +console.log(` Password: ${mainUser.password}\n`); +console.log(`Main piece: ${mainPieceId}`); +console.log(`Main edition: ${mainEditionId}\n`); +console.log('========================================================\n'); + +module.exports = { + mainUser, + mainPieceId, + mainEditionId +}; diff --git a/test/gemini/tests/main/authenticated.js b/test/gemini/tests/main/authenticated.js index bf579bff..52730d4c 100644 --- a/test/gemini/tests/main/authenticated.js +++ b/test/gemini/tests/main/authenticated.js @@ -1,6 +1,7 @@ 'use strict'; const gemini = require('gemini'); +const environment = require('../environment'); /** * Suite of tests against routes that require the user to be authenticated. @@ -25,8 +26,8 @@ gemini.suite('Authenticated', (suite) => { .capture('logged in', (actions, find) => { actions.waitForElementToShow('.ascribe-form', 5000); - actions.sendKeys(find('.ascribe-login-wrapper input[name=email]'), 'dimi@mailinator.com'); - actions.sendKeys(find('.ascribe-login-wrapper input[name=password]'), '0000000000'); + actions.sendKeys(find('.ascribe-login-wrapper input[name=email]'), environment.mainUser.email); + actions.sendKeys(find('.ascribe-login-wrapper input[name=password]'), environment.mainUser.password); actions.click(find('.ascribe-login-wrapper button[type=submit]')); actions.waitForElementToShow('.ascribe-accordion-list:not(.ascribe-loading-position)', 5000); diff --git a/test/gemini/tests/main/basic.js b/test/gemini/tests/main/basic.js index 317c5d84..2e3dcb3a 100644 --- a/test/gemini/tests/main/basic.js +++ b/test/gemini/tests/main/basic.js @@ -1,6 +1,7 @@ 'use strict'; const gemini = require('gemini'); +const environment = require('../environment'); /** * Basic suite of tests against routes that do not require the user to be authenticated. @@ -84,8 +85,8 @@ gemini.suite('Basic', (suite) => { // Remove hover from sign up link actions.click(emailInput); - actions.sendKeys(emailInput, 'dimi@mailinator.com'); - actions.sendKeys(find('.ascribe-form input[name=password]'), '0000000000'); + actions.sendKeys(emailInput, environment.mainUser.email); + actions.sendKeys(find('.ascribe-form input[name=password]'), environment.mainUser.password); }) .capture('login form filled', (actions, find) => { actions.click(find('.ascribe-form-header')); @@ -99,9 +100,9 @@ gemini.suite('Basic', (suite) => { actions.waitForElementToShow('.ascribe-form', 5000); }) .capture('sign up form filled with focus', (actions, find) => { - actions.sendKeys(find('.ascribe-form input[name=email]'), 'dimi@mailinator.com'); - actions.sendKeys(find('.ascribe-form input[name=password]'), '0000000000'); - actions.sendKeys(find('.ascribe-form input[name=password_confirm]'), '0000000000'); + actions.sendKeys(find('.ascribe-form input[name=email]'), environment.mainUser.email); + actions.sendKeys(find('.ascribe-form input[name=password]'), environment.mainUser.password); + actions.sendKeys(find('.ascribe-form input[name=password_confirm]'), environment.mainUser.password); }) .capture('sign up form filled with check', (actions, find) => { actions.click(find('.ascribe-form input[type="checkbox"] ~ .checkbox')); @@ -115,7 +116,7 @@ gemini.suite('Basic', (suite) => { actions.waitForElementToShow('.ascribe-form', 5000); }) .capture('password reset form filled with focus', (actions, find) => { - actions.sendKeys(find('.ascribe-form input[name="email"]'), 'dimi@mailinator.com'); + actions.sendKeys(find('.ascribe-form input[name="email"]'), environment.mainUser.email); }) .capture('password reset form filled', (actions, find) => { actions.click(find('.ascribe-form-header')); diff --git a/test/gemini/tests/main/detail.js b/test/gemini/tests/main/detail.js index 7adad5d4..fd07af6a 100644 --- a/test/gemini/tests/main/detail.js +++ b/test/gemini/tests/main/detail.js @@ -1,8 +1,9 @@ 'use strict'; const gemini = require('gemini'); -const pieceUrl = '/pieces/12374'; -const editionUrl = '/editions/14gw9x3VA9oJaxp4cHaAuK2bvJzvEj4Xvc'; +const environment = require('../environment'); +const pieceUrl = `/pieces/${environment.mainPieceId}`; +const editionUrl = `/editions/${environment.mainEditionId}`; /** * Suite of tests against the piece and edition routes. @@ -57,8 +58,8 @@ gemini.suite('Work detail', (suite) => { actions.waitForElementToShow('.ascribe-default-app', 5000); }) .capture('logged in', (actions, find) => { - actions.sendKeys(find('.ascribe-login-wrapper input[name=email]'), 'dimi@mailinator.com'); - actions.sendKeys(find('.ascribe-login-wrapper input[name=password]'), '0000000000'); + actions.sendKeys(find('.ascribe-login-wrapper input[name=email]'), environment.mainUser.email); + actions.sendKeys(find('.ascribe-login-wrapper input[name=password]'), environment.mainUser.password); actions.click(find('.ascribe-login-wrapper button[type=submit]')); actions.waitForElementToShow('.ascribe-accordion-list:not(.ascribe-loading-position)', 5000); diff --git a/test/gemini/tests/whitelabel/ikonotv/ikonotv.js b/test/gemini/tests/whitelabel/ikonotv/ikonotv.js index 1741aaa0..05c36405 100644 --- a/test/gemini/tests/whitelabel/ikonotv/ikonotv.js +++ b/test/gemini/tests/whitelabel/ikonotv/ikonotv.js @@ -1,6 +1,7 @@ 'use strict'; const gemini = require('gemini'); +const environment = require('../environment'); /** * Suite of tests against Cyland specific routes @@ -68,8 +69,8 @@ gemini.suite('Ikonotv', (suite) => { // Remove hover from sign up link actions.click(emailInput); - actions.sendKeys(emailInput, 'dimi@mailinator.com'); - actions.sendKeys(find('.ascribe-form input[name=password]'), '0000000000'); + actions.sendKeys(emailInput, environment.mainUser.email); + actions.sendKeys(find('.ascribe-form input[name=password]'), environment.mainUser.password); }) .capture('login form filled', (actions, find) => { actions.click(find('.ascribe-form-header')); @@ -81,9 +82,9 @@ gemini.suite('Ikonotv', (suite) => { .setUrl('/signup') .capture('sign up') .capture('sign up form filled with focus', (actions, find) => { - actions.sendKeys(find('.ascribe-form input[name=email]'), 'dimi@mailinator.com'); - actions.sendKeys(find('.ascribe-form input[name=password]'), '0000000000'); - actions.sendKeys(find('.ascribe-form input[name=password_confirm]'), '0000000000'); + actions.sendKeys(find('.ascribe-form input[name=email]'), environment.mainUser.email); + actions.sendKeys(find('.ascribe-form input[name=password]'), environment.mainUser.password); + actions.sendKeys(find('.ascribe-form input[name=password_confirm]'), environment.mainUser.password); }) .capture('sign up form filled with check', (actions, find) => { actions.click(find('.ascribe-form input[type="checkbox"] ~ .checkbox')); diff --git a/test/gemini/tests/whitelabel/shared/whitelabel_basic.js b/test/gemini/tests/whitelabel/shared/whitelabel_basic.js index 7fe5c256..24aa432b 100644 --- a/test/gemini/tests/whitelabel/shared/whitelabel_basic.js +++ b/test/gemini/tests/whitelabel/shared/whitelabel_basic.js @@ -1,6 +1,7 @@ 'use strict'; const gemini = require('gemini'); +const environment = require('../environment'); /** * Basic suite of tests against whitelabel routes that do not require authentication. @@ -43,8 +44,8 @@ gemini.suite('Whitelabel basic', (suite) => { // Remove hover from sign up link actions.click(emailInput); - actions.sendKeys(emailInput, 'dimi@mailinator.com'); - actions.sendKeys(find('.ascribe-form input[name=password]'), '0000000000'); + actions.sendKeys(emailInput, environment.mainUser.email); + actions.sendKeys(find('.ascribe-form input[name=password]'), environment.mainUser.password); }) .capture('login form filled', (actions, find) => { actions.click(find('.ascribe-form-header')); @@ -62,9 +63,9 @@ gemini.suite('Whitelabel basic', (suite) => { actions.wait(500); }) .capture('sign up form filled with focus', (actions, find) => { - actions.sendKeys(find('.ascribe-form input[name=email]'), 'dimi@mailinator.com'); - actions.sendKeys(find('.ascribe-form input[name=password]'), '0000000000'); - actions.sendKeys(find('.ascribe-form input[name=password_confirm]'), '0000000000'); + actions.sendKeys(find('.ascribe-form input[name=email]'), environment.mainUser.email); + actions.sendKeys(find('.ascribe-form input[name=password]'), environment.mainUser.password); + actions.sendKeys(find('.ascribe-form input[name=password_confirm]'), environment.mainUser.password); }) .capture('sign up form filled with check', (actions, find) => { actions.click(find('.ascribe-form input[type="checkbox"] ~ .checkbox')); @@ -80,7 +81,7 @@ gemini.suite('Whitelabel basic', (suite) => { actions.wait(500); }) .capture('password reset form filled with focus', (actions, find) => { - actions.sendKeys(find('.ascribe-form input[name="email"]'), 'dimi@mailinator.com'); + actions.sendKeys(find('.ascribe-form input[name="email"]'), environment.mainUser.email); }) .capture('password reset form filled', (actions, find) => { actions.click(find('.ascribe-form-header')); diff --git a/test/phantomjs/launch_app_and_login.js b/test/phantomjs/launch_app_and_login.js new file mode 100644 index 00000000..afc76f9e --- /dev/null +++ b/test/phantomjs/launch_app_and_login.js @@ -0,0 +1,71 @@ +'use strict'; + +var liveEnv = 'https://www.ascribe.io/app/login'; +// Note that if you are trying to access staging, you will need to use +// the --ignore-ssl-errors=true flag on phantomjs +var stagingEnv = 'https://www.ascribe.ninja/app/login'; +var localEnv = 'http://localhost.com:3000/login'; + +function launchAppAndLogin(env) { + console.log('Running test to launch ' + env + ' and log into the app'); + + var page = require('webpage').create(); + page.open(localEnv, function(status) { + var attemptedToLogIn; + var loginCheckInterval; + + console.log('Load ' + env + ': ' + status); + + if (status === 'success') { + console.log('Attempting to log in...'); + + attemptedToLogIn = page.evaluate(function () { + try { + var inputForm = document.querySelector('.ascribe-login-wrapper'); + var email = inputForm.querySelector('input[name=email]'); + var password = inputForm.querySelector('input[name=password]'); + var submitBtn = inputForm.querySelector('button[type=submit]'); + + email.value = 'dimi@mailinator.com'; + password.value = '0000000000'; + submitBtn.click(); + + return true; + } catch (ex) { + console.log('Error while trying to find login elements, not logging in.'); + return false; + } + }); + + if (attemptedToLogIn) { + loginCheckInterval = setInterval(function () { + var loggedIn = page.evaluate(function () { + // When they log in, they are taken to the collections page. + // When the piece list is loaded, the accordion list is either available or + // shows a placeholder, so let's check for these elements to determine + // when login is finished + return !!(document.querySelector('.ascribe-accordion-list:not(.ascribe-loading-position)') || + document.querySelector('.ascribe-accordion-list-placeholder')); + }); + + if (loggedIn) { + clearInterval(loginCheckInterval); + console.log('Successfully logged in.'); + console.log('Edit the onion/test/phantomjs/launch_app_and_login.js file to do further actions after logging in.'); + console.log('Stopping phantomJS...'); + phantom.exit(); + } + }, 1000); + } else { + console.log('Something happened while trying to log in, aborting...'); + phantom.exit(); + } + + } else { + console.log('Failed to load page, exiing...'); + phantom.exit(); + } + }); +} + +launchAppAndLogin(localEnv); From 8ef19a1ed0f7a0a9248d35e339b2985effab73fe Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Mon, 1 Feb 2016 17:24:19 +0100 Subject: [PATCH 54/94] Fix prize instances of the verify route to be coa_verify --- js/components/whitelabel/prize/prize_routes.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/components/whitelabel/prize/prize_routes.js b/js/components/whitelabel/prize/prize_routes.js index 5f80b30c..668bb1d5 100644 --- a/js/components/whitelabel/prize/prize_routes.js +++ b/js/components/whitelabel/prize/prize_routes.js @@ -57,7 +57,7 @@ const ROUTES = { headerTitle='COLLECTION'/> - + ), @@ -97,7 +97,7 @@ const ROUTES = { component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(SPSettingsContainer)}/> - + ) From afc7bc7735093a6d3f7a59b09932e9801e0aa1d4 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Mon, 1 Feb 2016 17:48:49 +0100 Subject: [PATCH 55/94] Fix npm scripts for domain specific visual regression tests --- package.json | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 5e72b863..2048656c 100644 --- a/package.json +++ b/package.json @@ -18,15 +18,16 @@ "vi-clean": "rm -rf ./gemini-report", "vi-phantom": "phantomjs --webdriver=4444", "vi-update": "gemini update -c ./test/gemini/.gemini.yml", - "vi-test": "npm run vi-clean && gemini test -c ./test/gemini/.gemini.yml --reporter html --reporter vflat || true", + "vi-test": "npm run vi-test:base || true", + "vi-test:base": "npm run vi-clean && gemini test -c ./test/gemini/.gemini.yml --reporter html --reporter vflat", "vi-test:all": "npm run vi-test", - "vi-test:main": "npm run vi-test -- --browser MainDesktop --browser MainMobile", - "vi-test:whitelabel": "GEMINI_BROWSERS='CcDesktop, CcMobile, CylandDesktop, CylandMobile, IkonotvDesktop, IkonotvMobile, LumenusDesktop, LumenusMobile, 23viviDesktop, 23viviMobile' npm run vi-test", - "vi-test:cc": "npm run vi-test -- --browser CcDesktop --browser CcMobile", - "vi-test:cyland": "npm run vi-test -- --browser CylandDesktop --browser CylandMobile", - "vi-test:ikonotv": "npm run vi-test -- --browser IkonotvDesktop --browser IkonotvMobile", - "vi-test:lumenus": "npm run vi-test -- --browser LumenusDesktop --browser LumenusMobile", - "vi-test:23vivi": "npm run vi-test -- --browser 23viviDesktop --browser 23viviMobile" + "vi-test:main": "npm run vi-test:base -- --browser MainDesktop --browser MainMobile || true", + "vi-test:whitelabel": "GEMINI_BROWSERS='CcDesktop, CcMobile, CylandDesktop, CylandMobile, IkonotvDesktop, IkonotvMobile, LumenusDesktop, LumenusMobile, 23viviDesktop, 23viviMobile' npm run vi-test:base || true", + "vi-test:cc": "npm run vi-test:base -- --browser CcDesktop --browser CcMobile", + "vi-test:cyland": "npm run vi-test:base -- --browser CylandDesktop --browser CylandMobile || true", + "vi-test:ikonotv": "npm run vi-test:base -- --browser IkonotvDesktop --browser IkonotvMobile || true", + "vi-test:lumenus": "npm run vi-test:base -- --browser LumenusDesktop --browser LumenusMobile || true", + "vi-test:23vivi": "npm run vi-test:base -- --browser 23viviDesktop --browser 23viviMobile || true" }, "browser": { "fineUploader": "./js/components/ascribe_uploader/vendor/s3.fine-uploader.js" From fd6371e2f1216746c099d660a4fafc6e09405eb1 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Mon, 1 Feb 2016 18:03:20 +0100 Subject: [PATCH 56/94] Small fixes for visual regression tests --- test/gemini/tests/main/authenticated.js | 3 ++- test/gemini/tests/whitelabel/ikonotv/ikonotv.js | 2 +- test/gemini/tests/whitelabel/shared/whitelabel_basic.js | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/test/gemini/tests/main/authenticated.js b/test/gemini/tests/main/authenticated.js index 52730d4c..5dc93562 100644 --- a/test/gemini/tests/main/authenticated.js +++ b/test/gemini/tests/main/authenticated.js @@ -101,7 +101,7 @@ gemini.suite('Authenticated', (suite) => { .capture('expanded edition in collection', (actions, find) => { actions.click(find('.ascribe-accordion-list-item .ascribe-accordion-list-item-edition-widget')); // Wait for editions to load - actions.waitForElementToShow('.ascribe-accordion-list-item-table', 5000); + actions.waitForElementToShow('.ascribe-accordion-list-item-table', 10000); }) gemini.suite('Collection placeholder', (collectionPlaceholderSuite) => { @@ -133,6 +133,7 @@ gemini.suite('Authenticated', (suite) => { .capture('piece list toolbar') .capture('piece list toolbar search filled', (actions, find) => { actions.sendKeys(find('.ascribe-piece-list-toolbar .search-bar input[type="text"]'), 'search text'); + actions.waitForElementToShow('.ascribe-piece-list-toolbar .search-bar .icon-ascribe-search', 5000); }) gemini.suite('Order widget dropdown', (pieceListToolbarOrderWidgetSuite) => { diff --git a/test/gemini/tests/whitelabel/ikonotv/ikonotv.js b/test/gemini/tests/whitelabel/ikonotv/ikonotv.js index 05c36405..5bf019d8 100644 --- a/test/gemini/tests/whitelabel/ikonotv/ikonotv.js +++ b/test/gemini/tests/whitelabel/ikonotv/ikonotv.js @@ -1,7 +1,7 @@ 'use strict'; const gemini = require('gemini'); -const environment = require('../environment'); +const environment = require('../../environment'); /** * Suite of tests against Cyland specific routes diff --git a/test/gemini/tests/whitelabel/shared/whitelabel_basic.js b/test/gemini/tests/whitelabel/shared/whitelabel_basic.js index 24aa432b..25713994 100644 --- a/test/gemini/tests/whitelabel/shared/whitelabel_basic.js +++ b/test/gemini/tests/whitelabel/shared/whitelabel_basic.js @@ -1,7 +1,7 @@ 'use strict'; const gemini = require('gemini'); -const environment = require('../environment'); +const environment = require('../../environment'); /** * Basic suite of tests against whitelabel routes that do not require authentication. From f09201d8c35a2346effa938427a8b31cecf7e7f5 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Thu, 4 Feb 2016 11:59:25 +0100 Subject: [PATCH 57/94] Use environment to configure timeouts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apparently node doesn’t accept destructuring yet… --- test/gemini/tests/environment.js | 33 +++++++++++----- test/gemini/tests/main/authenticated.js | 38 ++++++++++--------- test/gemini/tests/main/basic.js | 30 ++++++++------- test/gemini/tests/main/detail.js | 29 +++++++------- test/gemini/tests/whitelabel/23vivi/23vivi.js | 6 ++- test/gemini/tests/whitelabel/cyland/cyland.js | 6 ++- .../tests/whitelabel/ikonotv/ikonotv.js | 22 ++++++----- .../tests/whitelabel/lumenus/lumenus.js | 6 ++- .../whitelabel/shared/whitelabel_basic.js | 26 +++++++------ 9 files changed, 113 insertions(+), 83 deletions(-) diff --git a/test/gemini/tests/environment.js b/test/gemini/tests/environment.js index 4cb1dacf..32f1c8ee 100644 --- a/test/gemini/tests/environment.js +++ b/test/gemini/tests/environment.js @@ -1,22 +1,35 @@ 'use strict'; -const mainUser = { +const MAIN_USER = { email: 'dimi@mailinator.com', password: '0000000000' }; -const mainPieceId = '12374'; -const mainEditionId = '14gw9x3VA9oJaxp4cHaAuK2bvJzvEj4Xvc'; +const MAIN_PIECE_ID = '12374'; +const MAIN_EDITION_ID = '14gw9x3VA9oJaxp4cHaAuK2bvJzvEj4Xvc'; + +const TIMEOUTS = { + SHORT: 3000, + NORMAL: 5000, + LONG: 10000, + SUPER_DUPER_EXTRA_LONG: 30000 +}; console.log('================== Test environment ==================\n'); console.log('Main user:'); -console.log(` Email: ${mainUser.email}`); -console.log(` Password: ${mainUser.password}\n`); -console.log(`Main piece: ${mainPieceId}`); -console.log(`Main edition: ${mainEditionId}\n`); +console.log(` Email: ${MAIN_USER.email}`); +console.log(` Password: ${MAIN_USER.password}\n`); +console.log(`Main piece: ${MAIN_PIECE_ID}`); +console.log(`Main edition: ${MAIN_EDITION_ID}\n`); +console.log('Timeouts:'); +console.log(` Short: ${TIMEOUTS.SHORT}`); +console.log(` Normal: ${TIMEOUTS.NORMAL}\n`); +console.log(` Long: ${TIMEOUTS.LONG}\n`); +console.log(` Super super extra long: ${TIMEOUTS.SUPER_DUPER_EXTRA_LONG}\n`); console.log('========================================================\n'); module.exports = { - mainUser, - mainPieceId, - mainEditionId + MAIN_USER, + MAIN_PIECE_ID, + MAIN_EDITION_ID, + TIMEOUTS }; diff --git a/test/gemini/tests/main/authenticated.js b/test/gemini/tests/main/authenticated.js index 5dc93562..7adfe16d 100644 --- a/test/gemini/tests/main/authenticated.js +++ b/test/gemini/tests/main/authenticated.js @@ -2,6 +2,8 @@ const gemini = require('gemini'); const environment = require('../environment'); +const MAIN_USER = environment.MAIN_USER; +const TIMEOUTS = environment.TIMEOUTS; /** * Suite of tests against routes that require the user to be authenticated. @@ -15,7 +17,7 @@ gemini.suite('Authenticated', (suite) => { // also defines a `.before()` // FIXME: use a more generic class for this, like just '.app', // when we can use this file with the whitelabels - actions.waitForElementToShow('.ascribe-default-app', 5000); + actions.waitForElementToShow('.ascribe-default-app', TIMEOUTS.NORMAL); }); // Suite just to log us in before any other suites run @@ -24,13 +26,13 @@ gemini.suite('Authenticated', (suite) => { .setUrl('/login') .ignoreElements('.ascribe-body') .capture('logged in', (actions, find) => { - actions.waitForElementToShow('.ascribe-form', 5000); + actions.waitForElementToShow('.ascribe-form', TIMEOUTS.NORMAL); - actions.sendKeys(find('.ascribe-login-wrapper input[name=email]'), environment.mainUser.email); - actions.sendKeys(find('.ascribe-login-wrapper input[name=password]'), environment.mainUser.password); + actions.sendKeys(find('.ascribe-login-wrapper input[name=email]'), MAIN_USER.email); + actions.sendKeys(find('.ascribe-login-wrapper input[name=password]'), MAIN_USER.password); actions.click(find('.ascribe-login-wrapper button[type=submit]')); - actions.waitForElementToShow('.ascribe-accordion-list:not(.ascribe-loading-position)', 5000); + actions.waitForElementToShow('.ascribe-accordion-list:not(.ascribe-loading-position)', TIMEOUTS.NORMAL); }); }); @@ -41,7 +43,7 @@ gemini.suite('Authenticated', (suite) => { .ignoreElements('.client--cyland img.img-brand') .skip(/Mobile/) .before((actions, find) => { - actions.waitForElementToShow('.ascribe-accordion-list:not(.ascribe-loading-position)', 5000); + actions.waitForElementToShow('.ascribe-accordion-list:not(.ascribe-loading-position)', TIMEOUTS.NORMAL); }) .capture('desktop header'); @@ -70,7 +72,7 @@ gemini.suite('Authenticated', (suite) => { .ignoreElements('.client--cyland img.img-brand') .skip(/Desktop/) .before((actions, find) => { - actions.waitForElementToShow('.ascribe-accordion-list:not(.ascribe-loading-position)', 5000); + actions.waitForElementToShow('.ascribe-accordion-list:not(.ascribe-loading-position)', TIMEOUTS.NORMAL); }) .capture('mobile header') .capture('expanded mobile header', (actions, find) => { @@ -90,18 +92,18 @@ gemini.suite('Authenticated', (suite) => { collectionSuite .setCaptureElements('.ascribe-accordion-list') .before((actions, find) => { - actions.waitForElementToShow('.ascribe-accordion-list:not(.ascribe-loading-position)', 5000); + actions.waitForElementToShow('.ascribe-accordion-list:not(.ascribe-loading-position)', TIMEOUTS.NORMAL); // Wait for the images to load // FIXME: unfortuntately gemini doesn't support ignoring multiple elements from a single selector // so we're forced to wait and hope that the images will all finish loading after 5s. // We could also change the thumbnails with JS, but setting up a test user is probably easier. - actions.wait(5000); + actions.wait(TIMEOUTS.NORMAL); }) .capture('collection') .capture('expanded edition in collection', (actions, find) => { actions.click(find('.ascribe-accordion-list-item .ascribe-accordion-list-item-edition-widget')); // Wait for editions to load - actions.waitForElementToShow('.ascribe-accordion-list-item-table', 10000); + actions.waitForElementToShow('.ascribe-accordion-list-item-table', TIMEOUTS.LONG); }) gemini.suite('Collection placeholder', (collectionPlaceholderSuite) => { @@ -109,7 +111,7 @@ gemini.suite('Authenticated', (suite) => { .setCaptureElements('.ascribe-accordion-list-placeholder') .capture('collection empty search', (actions, find) => { actions.sendKeys(find('.ascribe-piece-list-toolbar .search-bar input[type="text"]'), 'no search result'); - actions.waitForElementToShow('.ascribe-accordion-list-placeholder', 5000); + actions.waitForElementToShow('.ascribe-accordion-list-placeholder', TIMEOUTS.NORMAL); }); }); @@ -119,7 +121,7 @@ gemini.suite('Authenticated', (suite) => { .capture('items selected', (actions, find) => { actions.click(find('.ascribe-accordion-list-item .ascribe-accordion-list-item-edition-widget')); // Wait for editions to load - actions.waitForElementToShow('.ascribe-accordion-list-item-table', 5000); + actions.waitForElementToShow('.ascribe-accordion-list-item-table', TIMEOUTS.NORMAL); actions.click('.ascribe-table thead tr input[type="checkbox"]'); actions.waitForElementToShow('.piece-list-bulk-modal'); @@ -133,7 +135,7 @@ gemini.suite('Authenticated', (suite) => { .capture('piece list toolbar') .capture('piece list toolbar search filled', (actions, find) => { actions.sendKeys(find('.ascribe-piece-list-toolbar .search-bar input[type="text"]'), 'search text'); - actions.waitForElementToShow('.ascribe-piece-list-toolbar .search-bar .icon-ascribe-search', 5000); + actions.waitForElementToShow('.ascribe-piece-list-toolbar .search-bar .icon-ascribe-search', TIMEOUTS.NORMAL); }) gemini.suite('Order widget dropdown', (pieceListToolbarOrderWidgetSuite) => { @@ -144,7 +146,7 @@ gemini.suite('Authenticated', (suite) => { actions.click(find('#ascribe-piece-list-toolbar-order-widget-dropdown')); // Wait as the dropdown screenshot still includes the collection in the background - actions.waitForElementToShow('.ascribe-accordion-list:not(.ascribe-loading-position)', 5000); + actions.waitForElementToShow('.ascribe-accordion-list:not(.ascribe-loading-position)', TIMEOUTS.NORMAL); }); }); @@ -156,7 +158,7 @@ gemini.suite('Authenticated', (suite) => { actions.click(find('#ascribe-piece-list-toolbar-filter-widget-dropdown')); // Wait as the dropdown screenshot still includes the collection in the background - actions.waitForElementToShow('.ascribe-accordion-list:not(.ascribe-loading-position)', 5000); + actions.waitForElementToShow('.ascribe-accordion-list:not(.ascribe-loading-position)', TIMEOUTS.NORMAL); }); }); }); @@ -167,7 +169,7 @@ gemini.suite('Authenticated', (suite) => { .capture('register work', (actions, find) => { // The uploader options are only rendered after the user is fetched, so // we have to wait for it here - actions.waitForElementToShow('.file-drag-and-drop-dialog .present-options', 5000); + actions.waitForElementToShow('.file-drag-and-drop-dialog .present-options', TIMEOUTS.NORMAL); }) .capture('register work filled', (actions, find) => { actions.sendKeys(find('.ascribe-form input[name="artist_name"]'), 'artist name'); @@ -199,7 +201,7 @@ gemini.suite('Authenticated', (suite) => { .before((actions, find) => { // This will be called before every nested suite begins unless that suite // also defines a `.before()` - actions.waitForElementToShow('.settings-container', 5000); + actions.waitForElementToShow('.settings-container', TIMEOUTS.NORMAL); }) .capture('user settings'); }); @@ -210,7 +212,7 @@ gemini.suite('Authenticated', (suite) => { .setUrl('/logout') .ignoreElements('.ascribe-body') .capture('logout', (actions, find) => { - actions.waitForElementToShow('.ascribe-login-wrapper', 10000); + actions.waitForElementToShow('.ascribe-login-wrapper', TIMEOUTS.LONG); }); }); }); diff --git a/test/gemini/tests/main/basic.js b/test/gemini/tests/main/basic.js index 2e3dcb3a..5657f77f 100644 --- a/test/gemini/tests/main/basic.js +++ b/test/gemini/tests/main/basic.js @@ -2,6 +2,8 @@ const gemini = require('gemini'); const environment = require('../environment'); +const MAIN_USER = environment.MAIN_USER; +const TIMEOUTS = environment.TIMEOUTS; /** * Basic suite of tests against routes that do not require the user to be authenticated. @@ -14,7 +16,7 @@ gemini.suite('Basic', (suite) => { // This will be called before every nested suite begins unless that suite // also defines a `.before()` // FIXME: use a more generic class for this, like just '.ascribe-app' - actions.waitForElementToShow('.ascribe-default-app', 5000); + actions.waitForElementToShow('.ascribe-default-app', TIMEOUTS.NORMAL); }); gemini.suite('Header-desktop', (headerSuite) => { @@ -22,7 +24,7 @@ gemini.suite('Basic', (suite) => { .setCaptureElements('nav.navbar .container') .skip(/Mobile/) .capture('desktop header', (actions, find) => { - actions.waitForElementToShow('nav.navbar .container', 5000); + actions.waitForElementToShow('nav.navbar .container', TIMEOUTS.NORMAL); }) .capture('hover on active item', (actions, find) => { const activeItem = find('nav.navbar li.active'); @@ -40,7 +42,7 @@ gemini.suite('Basic', (suite) => { .setCaptureElements('nav.navbar .container') .skip(/Desktop/) .capture('mobile header', (actions, find) => { - actions.waitForElementToShow('nav.navbar .container', 5000); + actions.waitForElementToShow('nav.navbar .container', TIMEOUTS.NORMAL); }) .capture('expanded mobile header', (actions, find) => { actions.click(find('nav.navbar .navbar-toggle')); @@ -56,7 +58,7 @@ gemini.suite('Basic', (suite) => { footerSuite .setCaptureElements('.ascribe-footer') .capture('footer', (actions, find) => { - actions.waitForElementToShow('.ascribe-footer', 5000); + actions.waitForElementToShow('.ascribe-footer', TIMEOUTS.NORMAL); }) .capture('hover on footer item', (actions, find) => { const footerItem = find('.ascribe-footer a:not(.social)'); @@ -71,7 +73,7 @@ gemini.suite('Basic', (suite) => { gemini.suite('Login', (loginSuite) => { loginSuite .capture('login', (actions, find) => { - actions.waitForElementToShow('.ascribe-form', 5000); + actions.waitForElementToShow('.ascribe-form', TIMEOUTS.NORMAL); }) .capture('hover on login submit', (actions, find) => { actions.mouseMove(find('.ascribe-form button[type=submit]')); @@ -85,8 +87,8 @@ gemini.suite('Basic', (suite) => { // Remove hover from sign up link actions.click(emailInput); - actions.sendKeys(emailInput, environment.mainUser.email); - actions.sendKeys(find('.ascribe-form input[name=password]'), environment.mainUser.password); + actions.sendKeys(emailInput, MAIN_USER.email); + actions.sendKeys(find('.ascribe-form input[name=password]'), MAIN_USER.password); }) .capture('login form filled', (actions, find) => { actions.click(find('.ascribe-form-header')); @@ -97,12 +99,12 @@ gemini.suite('Basic', (suite) => { signUpSuite .setUrl('/signup') .capture('sign up', (actions, find) => { - actions.waitForElementToShow('.ascribe-form', 5000); + actions.waitForElementToShow('.ascribe-form', TIMEOUTS.NORMAL); }) .capture('sign up form filled with focus', (actions, find) => { - actions.sendKeys(find('.ascribe-form input[name=email]'), environment.mainUser.email); - actions.sendKeys(find('.ascribe-form input[name=password]'), environment.mainUser.password); - actions.sendKeys(find('.ascribe-form input[name=password_confirm]'), environment.mainUser.password); + actions.sendKeys(find('.ascribe-form input[name=email]'), MAIN_USER.email); + actions.sendKeys(find('.ascribe-form input[name=password]'), MAIN_USER.password); + actions.sendKeys(find('.ascribe-form input[name=password_confirm]'), MAIN_USER.password); }) .capture('sign up form filled with check', (actions, find) => { actions.click(find('.ascribe-form input[type="checkbox"] ~ .checkbox')); @@ -113,10 +115,10 @@ gemini.suite('Basic', (suite) => { passwordResetSuite .setUrl('/password_reset') .capture('password reset', (actions, find) => { - actions.waitForElementToShow('.ascribe-form', 5000); + actions.waitForElementToShow('.ascribe-form', TIMEOUTS.NORMAL); }) .capture('password reset form filled with focus', (actions, find) => { - actions.sendKeys(find('.ascribe-form input[name="email"]'), environment.mainUser.email); + actions.sendKeys(find('.ascribe-form input[name="email"]'), MAIN_USER.email); }) .capture('password reset form filled', (actions, find) => { actions.click(find('.ascribe-form-header')); @@ -127,7 +129,7 @@ gemini.suite('Basic', (suite) => { coaVerifySuite .setUrl('/coa_verify') .capture('coa verify', (actions, find) => { - actions.waitForElementToShow('.ascribe-form', 5000); + actions.waitForElementToShow('.ascribe-form', TIMEOUTS.NORMAL); }) .capture('coa verify form filled with focus', (actions, find) => { actions.sendKeys(find('.ascribe-form input[name="message"]'), 'sample text'); diff --git a/test/gemini/tests/main/detail.js b/test/gemini/tests/main/detail.js index fd07af6a..39e83d70 100644 --- a/test/gemini/tests/main/detail.js +++ b/test/gemini/tests/main/detail.js @@ -2,8 +2,11 @@ const gemini = require('gemini'); const environment = require('../environment'); -const pieceUrl = `/pieces/${environment.mainPieceId}`; -const editionUrl = `/editions/${environment.mainEditionId}`; +const MAIN_USER = environment.MAIN_USER; +const TIMEOUTS = environment.TIMEOUTS; + +const pieceUrl = `/pieces/${environment.MAIN_PIECE_ID}`; +const editionUrl = `/editions/${environment.MAIN_EDITION_ID}`; /** * Suite of tests against the piece and edition routes. @@ -18,12 +21,12 @@ gemini.suite('Work detail', (suite) => { // also defines a `.before()` // FIXME: use a more generic class for this, like just '.app', // when we can use this file with the whitelabels - actions.waitForElementToShow('.ascribe-default-app', 5000); + actions.waitForElementToShow('.ascribe-default-app', TIMEOUTS.NORMAL); // Wait for the social media buttons to appear - actions.waitForElementToShow('.ascribe-social-button-list .fb-share-button iframe', 20000); - actions.waitForElementToShow('.ascribe-social-button-list .twitter-share-button', 20000); - actions.waitForElementToShow('.ascribe-media-player', 10000); + actions.waitForElementToShow('.ascribe-social-button-list .fb-share-button iframe', TIMEOUTS.SUPER_DUPER_EXTRA_LONG); + actions.waitForElementToShow('.ascribe-social-button-list .twitter-share-button', TIMEOUTS.SUPER_DUPER_EXTRA_LONG); + actions.waitForElementToShow('.ascribe-media-player', TIMEOUTS.LONG); }); gemini.suite('Basic piece', (basicPieceSuite) => { @@ -36,7 +39,7 @@ gemini.suite('Work detail', (suite) => { setCaptureElements('.shmui-wrap') .capture('shmui', (actions, find) => { actions.click(find('.ascribe-media-player')); - actions.waitForElementToShow('.shmui-wrap:not(.loading)', 30000); + actions.waitForElementToShow('.shmui-wrap:not(.loading)', TIMEOUTS.SUPER_DUPER_EXTRA_LONG); // Wait for the transition to end actions.wait(1000); }); @@ -55,14 +58,14 @@ gemini.suite('Work detail', (suite) => { .setUrl('/login') .ignoreElements('.ascribe-body') .before((actions, find) => { - actions.waitForElementToShow('.ascribe-default-app', 5000); + actions.waitForElementToShow('.ascribe-default-app', TIMEOUTS.NORMAL); }) .capture('logged in', (actions, find) => { - actions.sendKeys(find('.ascribe-login-wrapper input[name=email]'), environment.mainUser.email); - actions.sendKeys(find('.ascribe-login-wrapper input[name=password]'), environment.mainUser.password); + actions.sendKeys(find('.ascribe-login-wrapper input[name=email]'), MAIN_USER.email); + actions.sendKeys(find('.ascribe-login-wrapper input[name=password]'), MAIN_USER.password); actions.click(find('.ascribe-login-wrapper button[type=submit]')); - actions.waitForElementToShow('.ascribe-accordion-list:not(.ascribe-loading-position)', 5000); + actions.waitForElementToShow('.ascribe-accordion-list:not(.ascribe-loading-position)', TIMEOUTS.NORMAL); }); }); @@ -122,10 +125,10 @@ gemini.suite('Work detail', (suite) => { .setUrl('/logout') .ignoreElements('.ascribe-body') .before((actions, find) => { - actions.waitForElementToShow('.ascribe-default-app', 5000); + actions.waitForElementToShow('.ascribe-default-app', TIMEOUTS.NORMAL); }) .capture('logout', (actions, find) => { - actions.waitForElementToShow('.ascribe-login-wrapper', 10000); + actions.waitForElementToShow('.ascribe-login-wrapper', TIMEOUTS.LONG); }); }); }); diff --git a/test/gemini/tests/whitelabel/23vivi/23vivi.js b/test/gemini/tests/whitelabel/23vivi/23vivi.js index cafdfc6d..99efb310 100644 --- a/test/gemini/tests/whitelabel/23vivi/23vivi.js +++ b/test/gemini/tests/whitelabel/23vivi/23vivi.js @@ -1,6 +1,8 @@ 'use strict'; const gemini = require('gemini'); +const environment = require('../../environment'); +const TIMEOUTS = environment.TIMEOUTS; /** * Suite of tests against 23vivi specific routes @@ -11,7 +13,7 @@ gemini.suite('23vivi', (suite) => { .setCaptureElements('.ascribe-wallet-app') .before((actions, find) => { // This will be called before every nested suite begins - actions.waitForElementToShow('.ascribe-wallet-app', 5000); + actions.waitForElementToShow('.ascribe-wallet-app', TIMEOUTS.NORMAL); }); gemini.suite('Landing', (landingSuite) => { @@ -19,7 +21,7 @@ gemini.suite('23vivi', (suite) => { .setUrl('/') .capture('landing', (actions, find) => { // Wait for the logo to appear - actions.waitForElementToShow('.vivi23-landing--header-logo', 10000); + actions.waitForElementToShow('.vivi23-landing--header-logo', TIMEOUTS.LONG); }); }); diff --git a/test/gemini/tests/whitelabel/cyland/cyland.js b/test/gemini/tests/whitelabel/cyland/cyland.js index 06709f39..8159f53e 100644 --- a/test/gemini/tests/whitelabel/cyland/cyland.js +++ b/test/gemini/tests/whitelabel/cyland/cyland.js @@ -1,6 +1,8 @@ 'use strict'; const gemini = require('gemini'); +const environment = require('../../environment'); +const TIMEOUTS = environment.TIMEOUTS; /** * Suite of tests against Cyland specific routes @@ -11,7 +13,7 @@ gemini.suite('Cyland', (suite) => { .setCaptureElements('.ascribe-wallet-app') .before((actions, find) => { // This will be called before every nested suite begins - actions.waitForElementToShow('.ascribe-wallet-app', 5000); + actions.waitForElementToShow('.ascribe-wallet-app', TIMEOUTS.NORMAL); }); gemini.suite('Landing', (landingSuite) => { @@ -20,7 +22,7 @@ gemini.suite('Cyland', (suite) => { // Ignore Cyland's logo as it's a gif .ignoreElements('.cyland-landing img') .capture('landing', (actions, find) => { - actions.waitForElementToShow('.cyland-landing img', 10000); + actions.waitForElementToShow('.cyland-landing img', TIMEOUTS.LONG); }); }); diff --git a/test/gemini/tests/whitelabel/ikonotv/ikonotv.js b/test/gemini/tests/whitelabel/ikonotv/ikonotv.js index 5bf019d8..84d743c0 100644 --- a/test/gemini/tests/whitelabel/ikonotv/ikonotv.js +++ b/test/gemini/tests/whitelabel/ikonotv/ikonotv.js @@ -2,6 +2,8 @@ const gemini = require('gemini'); const environment = require('../../environment'); +const MAIN_USER = environment.MAIN_USER; +const TIMEOUTS = environment.TIMEOUTS; /** * Suite of tests against Cyland specific routes @@ -12,7 +14,7 @@ gemini.suite('Ikonotv', (suite) => { .setCaptureElements('.ascribe-wallet-app') .before((actions, find) => { // This will be called before every nested suite begins - actions.waitForElementToShow('.ascribe-wallet-app', 5000); + actions.waitForElementToShow('.ascribe-wallet-app', TIMEOUTS.NORMAL); }); gemini.suite('Landing', (landingSuite) => { @@ -30,7 +32,7 @@ gemini.suite('Ikonotv', (suite) => { }); // Wait for logo to appear - actions.waitForElementToShow('.ikonotv-landing header img', 10000); + actions.waitForElementToShow('.ikonotv-landing header img', TIMEOUTS.LONG); }); }); @@ -44,13 +46,13 @@ gemini.suite('Ikonotv', (suite) => { // also defines a `.before()` // FIXME: use a more generic class for this, like just '.app', // when we can use this file with the whitelabels - actions.waitForElementToShow('.ascribe-wallet-app', 5000); + actions.waitForElementToShow('.ascribe-wallet-app', TIMEOUTS.NORMAL); // Wait for the forms to appear - actions.waitForElementToShow('.ascribe-form', 5000); + actions.waitForElementToShow('.ascribe-form', TIMEOUTS.NORMAL); // Just use a dumb wait because the logo is set as a background image - actions.wait(3000); + actions.wait(TIMEOUTS.SHORT); }); gemini.suite('Login', (loginSuite) => { @@ -69,8 +71,8 @@ gemini.suite('Ikonotv', (suite) => { // Remove hover from sign up link actions.click(emailInput); - actions.sendKeys(emailInput, environment.mainUser.email); - actions.sendKeys(find('.ascribe-form input[name=password]'), environment.mainUser.password); + actions.sendKeys(emailInput, MAIN_USER.email); + actions.sendKeys(find('.ascribe-form input[name=password]'), MAIN_USER.password); }) .capture('login form filled', (actions, find) => { actions.click(find('.ascribe-form-header')); @@ -82,9 +84,9 @@ gemini.suite('Ikonotv', (suite) => { .setUrl('/signup') .capture('sign up') .capture('sign up form filled with focus', (actions, find) => { - actions.sendKeys(find('.ascribe-form input[name=email]'), environment.mainUser.email); - actions.sendKeys(find('.ascribe-form input[name=password]'), environment.mainUser.password); - actions.sendKeys(find('.ascribe-form input[name=password_confirm]'), environment.mainUser.password); + actions.sendKeys(find('.ascribe-form input[name=email]'), MAIN_USER.email); + actions.sendKeys(find('.ascribe-form input[name=password]'), MAIN_USER.password); + actions.sendKeys(find('.ascribe-form input[name=password_confirm]'), MAIN_USER.password); }) .capture('sign up form filled with check', (actions, find) => { actions.click(find('.ascribe-form input[type="checkbox"] ~ .checkbox')); diff --git a/test/gemini/tests/whitelabel/lumenus/lumenus.js b/test/gemini/tests/whitelabel/lumenus/lumenus.js index a9ff53cd..8e8f568d 100644 --- a/test/gemini/tests/whitelabel/lumenus/lumenus.js +++ b/test/gemini/tests/whitelabel/lumenus/lumenus.js @@ -1,6 +1,8 @@ 'use strict'; const gemini = require('gemini'); +const environment = require('../../environment'); +const TIMEOUTS = environment.TIMEOUTS; /** * Suite of tests against lumenus specific routes @@ -11,7 +13,7 @@ gemini.suite('Lumenus', (suite) => { .setCaptureElements('.ascribe-wallet-app') .before((actions, find) => { // This will be called before every nested suite begins - actions.waitForElementToShow('.ascribe-wallet-app', 5000); + actions.waitForElementToShow('.ascribe-wallet-app', TIMEOUTS.NORMAL); }); gemini.suite('Landing', (landingSuite) => { @@ -19,7 +21,7 @@ gemini.suite('Lumenus', (suite) => { .setUrl('/') .capture('landing', (actions, find) => { // Wait for the logo to appear - actions.waitForElementToShow('.wp-landing-wrapper img', 10000); + actions.waitForElementToShow('.wp-landing-wrapper img', TIMEOUTS.LONG); }); }); diff --git a/test/gemini/tests/whitelabel/shared/whitelabel_basic.js b/test/gemini/tests/whitelabel/shared/whitelabel_basic.js index 25713994..0d5ac26b 100644 --- a/test/gemini/tests/whitelabel/shared/whitelabel_basic.js +++ b/test/gemini/tests/whitelabel/shared/whitelabel_basic.js @@ -2,6 +2,8 @@ const gemini = require('gemini'); const environment = require('../../environment'); +const MAIN_USER = environment.MAIN_USER; +const TIMEOUTS = environment.TIMEOUTS; /** * Basic suite of tests against whitelabel routes that do not require authentication. @@ -13,7 +15,7 @@ gemini.suite('Whitelabel basic', (suite) => { // This will be called before every nested suite begins unless that suite // also defines a `.before()` // FIXME: use a more generic class for this, like just '.ascribe-app' - actions.waitForElementToShow('.ascribe-wallet-app', 5000); + actions.waitForElementToShow('.ascribe-wallet-app', TIMEOUTS.NORMAL); // Use a dumb wait in case we're still waiting for other assets, like fonts, to load actions.wait(1000); @@ -25,12 +27,12 @@ gemini.suite('Whitelabel basic', (suite) => { // See Ikono .skip(/Ikono/) .capture('login', (actions, find) => { - actions.waitForElementToShow('.ascribe-form', 5000); + actions.waitForElementToShow('.ascribe-form', TIMEOUTS.NORMAL); // For some reason, the screenshots seem to keep catching the whitelabel login form // on a refresh and without fonts loaded (maybe because they're the first tests run // and the cache isn't hot yet?). // Let's wait a bit and hope they load. - actions.wait(3000); + actions.wait(TIMEOUTS.SHORT); }) .capture('hover on login submit', (actions, find) => { actions.mouseMove(find('.ascribe-form button[type=submit]')); @@ -44,8 +46,8 @@ gemini.suite('Whitelabel basic', (suite) => { // Remove hover from sign up link actions.click(emailInput); - actions.sendKeys(emailInput, environment.mainUser.email); - actions.sendKeys(find('.ascribe-form input[name=password]'), environment.mainUser.password); + actions.sendKeys(emailInput, MAIN_USER.email); + actions.sendKeys(find('.ascribe-form input[name=password]'), MAIN_USER.password); }) .capture('login form filled', (actions, find) => { actions.click(find('.ascribe-form-header')); @@ -58,14 +60,14 @@ gemini.suite('Whitelabel basic', (suite) => { // See Ikono .skip(/Ikono/) .capture('sign up', (actions, find) => { - actions.waitForElementToShow('.ascribe-form', 5000); + actions.waitForElementToShow('.ascribe-form', TIMEOUTS.NORMAL); // Wait in case the form reloads due to other assets loading actions.wait(500); }) .capture('sign up form filled with focus', (actions, find) => { - actions.sendKeys(find('.ascribe-form input[name=email]'), environment.mainUser.email); - actions.sendKeys(find('.ascribe-form input[name=password]'), environment.mainUser.password); - actions.sendKeys(find('.ascribe-form input[name=password_confirm]'), environment.mainUser.password); + actions.sendKeys(find('.ascribe-form input[name=email]'), MAIN_USER.email); + actions.sendKeys(find('.ascribe-form input[name=password]'), MAIN_USER.password); + actions.sendKeys(find('.ascribe-form input[name=password_confirm]'), MAIN_USER.password); }) .capture('sign up form filled with check', (actions, find) => { actions.click(find('.ascribe-form input[type="checkbox"] ~ .checkbox')); @@ -76,12 +78,12 @@ gemini.suite('Whitelabel basic', (suite) => { passwordResetSuite .setUrl('/password_reset') .capture('password reset', (actions, find) => { - actions.waitForElementToShow('.ascribe-form', 5000); + actions.waitForElementToShow('.ascribe-form', TIMEOUTS.NORMAL); // Wait in case the form reloads due to other assets loading actions.wait(500); }) .capture('password reset form filled with focus', (actions, find) => { - actions.sendKeys(find('.ascribe-form input[name="email"]'), environment.mainUser.email); + actions.sendKeys(find('.ascribe-form input[name="email"]'), MAIN_USER.email); }) .capture('password reset form filled', (actions, find) => { actions.click(find('.ascribe-form-header')); @@ -92,7 +94,7 @@ gemini.suite('Whitelabel basic', (suite) => { coaVerifySuite .setUrl('/coa_verify') .capture('coa verify', (actions, find) => { - actions.waitForElementToShow('.ascribe-form', 5000); + actions.waitForElementToShow('.ascribe-form', TIMEOUTS.NORMAL); // Wait in case the form reloads due to other assets loading actions.wait(500); }) From 02c25c323d74a0711fbe73a26cc929ed0ae0574d Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Thu, 4 Feb 2016 12:00:05 +0100 Subject: [PATCH 58/94] Ignore locally generated screenshots Too many minor differences with PhantomJS between OSX/Linux --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 0b4d85c7..e497465f 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ build/* gemini-coverage/* gemini-report/* +test/gemini/screenshots/* node_modules/* From 6362a3651f0288f912e0a22d6ea5859626357d62 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Thu, 4 Feb 2016 12:02:12 +0100 Subject: [PATCH 59/94] Remove phantomJS test file --- test/gemini/README.md | 6 +-- test/phantomjs/launch_app_and_login.js | 71 -------------------------- 2 files changed, 3 insertions(+), 74 deletions(-) delete mode 100644 test/phantomjs/launch_app_and_login.js diff --git a/test/gemini/README.md b/test/gemini/README.md index bc029041..3521056e 100644 --- a/test/gemini/README.md +++ b/test/gemini/README.md @@ -190,9 +190,9 @@ current execution state of that breakpoint on the page you're on. --- -To simplify triaging simple issues and test if everything is working, I've added a short test script that can be run -with PhantomJS to check if it can access the web app and log in. You can edit the `lauch_app_and_login.js` file to -change the environment to run against. +To simplify triaging simple issues and test if everything is working, The repo had a short test script that can be run +with PhantomJS to check if it can access the web app and log in. Find `onion/test/phantomjs/launch_app_and_login.js` in +the repo's history, restore it, and then run: ```bash # In root /onion folder diff --git a/test/phantomjs/launch_app_and_login.js b/test/phantomjs/launch_app_and_login.js deleted file mode 100644 index afc76f9e..00000000 --- a/test/phantomjs/launch_app_and_login.js +++ /dev/null @@ -1,71 +0,0 @@ -'use strict'; - -var liveEnv = 'https://www.ascribe.io/app/login'; -// Note that if you are trying to access staging, you will need to use -// the --ignore-ssl-errors=true flag on phantomjs -var stagingEnv = 'https://www.ascribe.ninja/app/login'; -var localEnv = 'http://localhost.com:3000/login'; - -function launchAppAndLogin(env) { - console.log('Running test to launch ' + env + ' and log into the app'); - - var page = require('webpage').create(); - page.open(localEnv, function(status) { - var attemptedToLogIn; - var loginCheckInterval; - - console.log('Load ' + env + ': ' + status); - - if (status === 'success') { - console.log('Attempting to log in...'); - - attemptedToLogIn = page.evaluate(function () { - try { - var inputForm = document.querySelector('.ascribe-login-wrapper'); - var email = inputForm.querySelector('input[name=email]'); - var password = inputForm.querySelector('input[name=password]'); - var submitBtn = inputForm.querySelector('button[type=submit]'); - - email.value = 'dimi@mailinator.com'; - password.value = '0000000000'; - submitBtn.click(); - - return true; - } catch (ex) { - console.log('Error while trying to find login elements, not logging in.'); - return false; - } - }); - - if (attemptedToLogIn) { - loginCheckInterval = setInterval(function () { - var loggedIn = page.evaluate(function () { - // When they log in, they are taken to the collections page. - // When the piece list is loaded, the accordion list is either available or - // shows a placeholder, so let's check for these elements to determine - // when login is finished - return !!(document.querySelector('.ascribe-accordion-list:not(.ascribe-loading-position)') || - document.querySelector('.ascribe-accordion-list-placeholder')); - }); - - if (loggedIn) { - clearInterval(loginCheckInterval); - console.log('Successfully logged in.'); - console.log('Edit the onion/test/phantomjs/launch_app_and_login.js file to do further actions after logging in.'); - console.log('Stopping phantomJS...'); - phantom.exit(); - } - }, 1000); - } else { - console.log('Something happened while trying to log in, aborting...'); - phantom.exit(); - } - - } else { - console.log('Failed to load page, exiing...'); - phantom.exit(); - } - }); -} - -launchAppAndLogin(localEnv); From 95bbe410dea171071fc3fcf87bccb5eba775b267 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Thu, 4 Feb 2016 13:28:49 +0100 Subject: [PATCH 60/94] Rebase remerge --- README.md | 69 ++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 50 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 478562d3..508e84d5 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,19 @@ Introduction ============ -Onion is the web client for Ascribe. The idea is to have a well documented, -easy to test, easy to hack, JavaScript application. +Onion is the web client for Ascribe. The idea is to have a well documented, modern, easy to test, easy to hack, JavaScript application. -The code is JavaScript ECMA 6. +The code is JavaScript 2015 / ECMAScript 6. Getting started =============== + Install some nice extension for Chrom(e|ium): - [React Developer Tools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi) - +- [Alt Developer Tools](https://github.com/goatslacker/alt-devtool) + ```bash git clone git@github.com:ascribe/onion.git cd onion @@ -37,17 +38,34 @@ Additionally, to work on the white labeling functionality, you need to edit your JavaScript Code Conventions =========================== + For this project, we're using: * 4 Spaces -* We use ES6 +* ES6 * We don't use ES6's class declaration for React components because it does not support Mixins as well as Autobinding ([Blog post about it](http://facebook.github.io/react/blog/2015/01/27/react-v0.13.0-beta-1.html#autobinding)) * We don't use camel case for file naming but in everything Javascript related -* We use `let` instead of `var`: [SA Post](http://stackoverflow.com/questions/762011/javascript-let-keyword-vs-var-keyword) -* We don't use Javascript's `Date` object, as its interface introduced bugs previously and we're including `momentjs` for other dependencies anyways +* We use `momentjs` instead of Javascript's `Date` object, as the native `Date` interface previously introduced bugs and we're including `momentjs` for other dependencies anyway + +Make sure to check out the [style guide](https://github.com/ascribe/javascript). + +Linting +------- + +We use [ESLint](https://github.com/eslint/eslint) with our own [custom ruleset](.eslintrc). + + +SCSS Code Conventions +===================== + +Install [lint-scss](https://github.com/brigade/scss-lint), check the [editor integration docs](https://github.com/brigade/scss-lint#editor-integration) to integrate the lint in your editor. + +Some interesting links: +* [Improving Sass code quality on theguardian.com](https://www.theguardian.com/info/developer-blog/2014/may/13/improving-sass-code-quality-on-theguardiancom) + Branch names -===================== +============ To allow Github and JIRA to track branches while still allowing us to switch branches quickly using a ticket's number (and keep our peace of mind), we have the following rules for naming branches: @@ -61,22 +79,21 @@ AD--brief-and-sane-description-of-the-ticket where `brief-and-sane-description-of-the-ticket` does not need to equal to the issue or ticket's title. + Example -------------- +------- + **JIRA ticket name:** `AD-1242 - Frontend caching for simple endpoints to measure perceived page load ` **Github branch name:** `AD-1242-caching-solution-for-stores` -SCSS Code Conventions -===================== -Install [lint-scss](https://github.com/brigade/scss-lint), check the [editor integration docs](https://github.com/brigade/scss-lint#editor-integration) to integrate the lint in your editor. - -Some interesting links: -* [Improving Sass code quality on theguardian.com](https://www.theguardian.com/info/developer-blog/2014/may/13/improving-sass-code-quality-on-theguardiancom) - Testing -=============== +======= + +Unit Testing +------------ + We're using Facebook's jest to do testing as it integrates nicely with react.js as well. Tests are always created per directory by creating a `__tests__` folder. To test a specific file, a `_tests.js` file needs to be created. @@ -86,7 +103,15 @@ This is due to the fact that jest's function mocking and ES6 module syntax are [ Therefore, to require a module in your test file, you need to use CommonJS's `require` syntax. Except for this, all tests can be written in ES6 syntax. -## Workflow +Visual Regression Testing +------------------------- + +We're using [Gemini](https://github.com/gemini-testing/gemini) for visual regression tests because it supports PhantomJS and SauceLabs. + + +Workflow +======== + Generally, when you're runing `gulp serve`, all tests are being run. If you want to test exclusively (without having the obnoxious ES6Linter warnings), you can just run `gulp jest:watch`. @@ -137,9 +162,16 @@ A: Easily by starting the your gulp process with the following command: ONION_BASE_URL='/' ONION_SERVER_URL='http://localhost.com:8000/' gulp serve ``` +Or, by adding these two your environment variables: +``` +ONION_BASE_URL='/' +ONION_SERVER_URL='http://localhost.com:8000/' +``` + Q: I want to know all dependencies that get bundled into the live build. A: ```browserify -e js/app.js --list > webapp-dependencies.txt``` + Reading list ============ @@ -152,7 +184,6 @@ Start here - [alt.js](http://alt.js.org/) - [alt.js readme](https://github.com/goatslacker/alt) - Moar stuff ---------- From bd53151964897268c17736920f6090fe0c45f054 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Wed, 20 Jan 2016 21:18:24 +0100 Subject: [PATCH 61/94] Add Gemini configuration for desktop and mobile on phantoms --- .gemini.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .gemini.yml diff --git a/.gemini.yml b/.gemini.yml new file mode 100644 index 00000000..5af1ef27 --- /dev/null +++ b/.gemini.yml @@ -0,0 +1,14 @@ +rootUrl: https://www.ascribe.ninja/app + +browsers: + PhantomJSDesktop: + screenshotsDir: './gemini/screens/desktop' + windowSize: 1900x1080 + desiredCapabilities: + browserName: phantomjs + + PhantomJSMobile: + screenshotsDir: './gemini/screens/mobile' + windowSize: 767x1364 + desiredCapabilities: + browserName: phantomjs From 3594229a6eb62518691088132db25171d4c5d1b9 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Thu, 4 Feb 2016 13:29:33 +0100 Subject: [PATCH 62/94] Rebase remerge --- .gitignore | 9 ++- README.md | 4 +- docs/visual-regression-testing.md | 126 ++++++++++++++++++++++++++++++ package.json | 2 + phantomjs/launch_app_and_login.js | 62 +++++++++++++++ 5 files changed, 199 insertions(+), 4 deletions(-) create mode 100644 docs/visual-regression-testing.md create mode 100644 phantomjs/launch_app_and_login.js diff --git a/.gitignore b/.gitignore index f5bf11e8..0b4d85c7 100644 --- a/.gitignore +++ b/.gitignore @@ -16,10 +16,13 @@ webapp-dependencies.txt pids logs results - -node_modules/* -build +build/* + +gemini-coverage/* +gemini-report/* + +node_modules/* .DS_Store .env diff --git a/README.md b/README.md index 508e84d5..1b918e5d 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,9 @@ Therefore, to require a module in your test file, you need to use CommonJS's `re Visual Regression Testing ------------------------- -We're using [Gemini](https://github.com/gemini-testing/gemini) for visual regression tests because it supports PhantomJS and SauceLabs. +We're using [Gemini](https://github.com/gemini-testing/gemini) for visual regression tests because it supports both PhantomJS2 and SauceLabs. + +See the [helper docs](docs/visual-regression-testing.md) for information on installing Gemini, its dependencies, and running and writing tests. Workflow diff --git a/docs/visual-regression-testing.md b/docs/visual-regression-testing.md new file mode 100644 index 00000000..30a711ff --- /dev/null +++ b/docs/visual-regression-testing.md @@ -0,0 +1,126 @@ +Introduction +============ + +When in doubt, see [Gemini](https://github.com/gemini-testing/gemini) and [their docs](https://github.com/gemini-testing/gemini/tree/master/doc) for more information as well as configuration options. + +Contents +======== + + 1. [Installation](#installation) + 1. [Running Tests](#running-tests) + 1. [Gemini Usage](#gemini-usage) + 1. [PhantomJS](#phantomjs) + + +Installation +============ + +First install [PhantomJS2](https://www.npmjs.com/package/phantomjs2): + +```bash +npm install -g phantomjs2 + +# If using OSX, install upx and decompress the binary downloaded by npm manually: +brew install upx +# Navigate to the binary, ie. /Users/Brett/.nvm/v5.4.0/lib/node_modules/phantomjs2/lib/phantom/bin +upx -d phantomjs + +``` + +Then [install Gemini globally and locally with npm](https://github.com/gemini-testing/gemini/blob/master/README.md#installation). + + +Running Tests +============= + +Run PhantomJS: + +```bash +phantomjs --webdriver=4444 +``` + +And then run Gemini tests: + +```bash +# In root onion/ +gemini test gemini/* --report html +``` + +If you've made changes and want them to be the new baseline (ie. it's a correct change--**make sure** to test there are no regressions first!), use + +```bash +# In root onion/ +gemini update gemini/* +``` + + +Gemini Usage +============ + +While Gemini itself is easy to use on simple, static pages, there are some nice to knows when dealing with a single page app like ours (where much of it is behind an authentication barrier as well). + +Authentication +-------------- + +Authentication presents a tricky problem with Gemini, since we can't inject any cookies or even run a start up script through the browser before letting Gemini hook in. The solution is to script the log in process through Gemini, and put waits for the log in to succeed, before testing parts of the app that require the authentication. + +Browser Session States +---------------------- + +Gemini will start a new instance of the browser for each browser configuration defined in the .gemini.yml file when Gemini's launched (ie. `gemini update`, `gemini test`, etc). + +Although each new suite will cause the testing browser to be refreshed, the above means that cookies and other persistent state will be kept across suites for a browser across all runs, even if the suites are from different files. + +**What this comes down to is**: once you've logged in, you'll stay logged in until you decide to log out or the running instance of Gemini ends. In general practice, it's a good idea to clear the state of the app at the end of each suite of tests by logging out. + +(**Note**: Persistent storage, such as local storage, has not been explicitly tested as to whether they are kept, but as the cookies are cleared each time, this seems unlikely) + +Test Reporting +-------------- + +Using the `--report html` flag with Gemini will produce a webpage with the test's results in /gemini-report that will show the old, new, and diff images. Using this is highly recommended (and fun!). + + +PhantomJS +========= + +[PhantomJS](http://phantomjs.org/) is a headless browser that allows us to run tests and take screenshots without needing a browser. + +Its second version (PhantomJS2) uses a much more recent version of Webkit, and is a big reason why Gemini (as opposed to other utilities, ie. PhantomCSS) was chosen. Due to the large number of breaking changes introduced between PhantomJS 1.9 to 2.0, a large number of tools (ie. CasperJS) are, at the time of writing, lacking support for 2.0. + +While you don't need to know too much about PhantomJS to use and write Gemini tests, there are still a number of useful things to know about. + +Useful features +--------------- + +You can find the full list of CLI commands in the [documentation](http://phantomjs.org/api/command-line.html). + +Flags that are of particular interest to us: + * `--webdriver=4444`: sets the webdriver port to be 4444, the default webdriver port that Gemini expects. + * `--ignore-ssl-errors=true`: ignores any SSL errors that may occur. Particular useful when hooking up the tests to staging, as the certificate we use is self-signed. + * `--ssl-protocol=any`: allows any ssl protocol to be used. May be useful when `--ignore-ssl-errors=true` doesn't work. + * '--remote-debugger-port`: allows for remote debugging the running PhantomJS instance. More on this later. + +Troubleshooting and Debugging +----------------------------- + +Remote debugging is possible with PhantomJS using the `--remote-debugger-port` option. See the [troubleshooting docs](http://phantomjs.org/troubleshooting.html). + +To begin using it, add `debugger;` statements to the file being run by `phantomjs`, and access the port number specified after `--remote-debugger-port` on localhost: + +```bash +phantomjs --remote-debugger-port=9000 debug.js +``` + +PhantomJS will start and then immediately breakpoint. Go to http://localhost:9000/webkit/inspector/inspector.html?page=1 and then to its console tab. Go to your first breakpoint (the first `debugger;` statement executed) by running `__run()` in the console tab. Subsequent breakpoints can be reached by successively running `__run()` in that same console tab. + +At each breakpoint, you can to http://localhost:9000 on a new browser tab and click on one of the links to go to the current execution state of that breakpoint on the page you're on. + +--- + +To simplify triaging simple issues and test if everything is working, I've added a short test script that can be run with PhantomJS to check if it can access the web app and log in. You can edit the `lauch_app_and_login.js` file to change the environment to run against. + +```bash +# In root /onion folder +phantomjs phantomjs/launch_app_and_login.js +``` diff --git a/package.json b/package.json index 091203b5..d8e76da5 100644 --- a/package.json +++ b/package.json @@ -42,8 +42,10 @@ "chai-as-promised": "^5.1.0", "colors": "^1.1.2", "dotenv": "^1.2.0", + "gemini": "^2.0.3", "jest-cli": "^0.4.0", "mocha": "^2.3.4", + "phantomjs2": "^2.0.2", "sauce-connect-launcher": "^0.13.0", "wd": "^0.4.0" }, diff --git a/phantomjs/launch_app_and_login.js b/phantomjs/launch_app_and_login.js new file mode 100644 index 00000000..e5418519 --- /dev/null +++ b/phantomjs/launch_app_and_login.js @@ -0,0 +1,62 @@ +'use strict'; + +var liveEnv = 'https://www.ascribe.io/app/login'; +// Note that if you are trying to access staging, you will need to use +// the --ignore-ssl-errors=true flag on phantomjs +var stagingEnv = 'https://www.ascribe.ninja/app/login'; +var localEnv = 'http://localhost.com:3000/login'; + +var page = require('webpage').create(); +page.open(localEnv, function(status) { + var attemptedToLogIn; + var loginCheckInterval; + + console.log('Status: ' + status); + + if (status === 'success') { + console.log('Attempting to log in...'); + + attemptedToLogIn = page.evaluate(function () { + try { + var inputForm = document.querySelector('.ascribe-login-wrapper'); + var email = inputForm.querySelector('input[type=email]'); + var password = inputForm.querySelector('input[type=password]'); + var submitBtn = inputForm.querySelector('button[type=submit]'); + + email.value = 'dimi@mailinator.com'; + password.value = '0000000000'; + submitBtn.click(); + + return true; + } catch (ex) { + console.log('Error while trying to find login elements, not logging in.'); + return false; + } + }); + + if (attemptedToLogIn) { + loginCheckInterval = setInterval(function () { + var loggedIn = page.evaluate(function () { + // When they log in, they are taken to the collections page. + // When the piece list is loaded, the accordion list is either available or + // shows a placeholder, so let's check for these elements to determine + // when login is finished + return !!(document.querySelector('.ascribe-accordion-list:not(.ascribe-loading-position)') || + document.querySelector('.ascribe-accordion-list-placeholder')); + }); + + if (loggedIn) { + clearInterval(loginCheckInterval); + console.log('Successfully logged in.'); + } + }, 1000); + } else { + console.log('Something happened while trying to log in, aborting...'); + phantom.exit(); + } + + } else { + console.log('Failed to load page, exiing...'); + phantom.exit(); + } +}); From cb59d042b5c898490117d547a499d3af65784ee3 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Fri, 29 Jan 2016 11:17:30 +0100 Subject: [PATCH 63/94] Update docs for PhantomJS 2.1, whitespace, and more tips --- docs/visual-regression-testing.md | 107 +++++++++++++++++++++++------- 1 file changed, 82 insertions(+), 25 deletions(-) diff --git a/docs/visual-regression-testing.md b/docs/visual-regression-testing.md index 30a711ff..d544b1fa 100644 --- a/docs/visual-regression-testing.md +++ b/docs/visual-regression-testing.md @@ -1,33 +1,39 @@ Introduction ============ -When in doubt, see [Gemini](https://github.com/gemini-testing/gemini) and [their docs](https://github.com/gemini-testing/gemini/tree/master/doc) for more information as well as configuration options. +When in doubt, see [Gemini](https://github.com/gemini-testing/gemini) and [their +docs](https://github.com/gemini-testing/gemini/tree/master/doc) for more information as well as configuration options. Contents ======== 1. [Installation](#installation) 1. [Running Tests](#running-tests) - 1. [Gemini Usage](#gemini-usage) + 1. [Gemini Usage and Writing Tests](#gemini-usage-and-writing-tests) 1. [PhantomJS](#phantomjs) + 1. [TODO](#todo) Installation ============ -First install [PhantomJS2](https://www.npmjs.com/package/phantomjs2): +First make sure that you're using NodeJS 5.0+ as the tests are written using ES6 syntax. + +Then, install [PhantomJS2](https://www.npmjs.com/package/phantomjs2): ```bash -npm install -g phantomjs2 +# Until phantomjs2 is updated for the new 2.1 version of PhantomJS, use the following (go to https://bitbucket.org/ariya/phantomjs/downloads to find a build for your OS) +npm install -g phantomjs2 --phantomjs_downloadurl=https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-macosx.zip -# If using OSX, install upx and decompress the binary downloaded by npm manually: +# If using OSX, you may have to install upx and decompress the binary downloaded by npm manually: brew install upx -# Navigate to the binary, ie. /Users/Brett/.nvm/v5.4.0/lib/node_modules/phantomjs2/lib/phantom/bin + +# Navigate to the binary, ie. /Users/Brett/.nvm/versions/node/v5.4.0/lib/node_modules/phantomjs2/lib/phantom/bin/phantomjs upx -d phantomjs ``` -Then [install Gemini globally and locally with npm](https://github.com/gemini-testing/gemini/blob/master/README.md#installation). +Finally, [install Gemini globally and locally with npm](https://github.com/gemini-testing/gemini/blob/master/README.md#installation). Running Tests @@ -54,41 +60,84 @@ gemini update gemini/* ``` -Gemini Usage -============ +Gemini Usage and Writing Tests +============================== -While Gemini itself is easy to use on simple, static pages, there are some nice to knows when dealing with a single page app like ours (where much of it is behind an authentication barrier as well). +While Gemini itself is easy to use on simple, static pages, there are some nice to knows when dealing with a single page +app like ours (where much of it is behind an authentication barrier as well). + +Command Line Interface +---------------------- + +See [the docs](https://github.com/gemini-testing/gemini/blob/master/doc/commands.md) on the commands that are available. +`npm run vi-*` is set up with some of these commands, but you may want to build your own or learn about some of the +other functions. Authentication -------------- -Authentication presents a tricky problem with Gemini, since we can't inject any cookies or even run a start up script through the browser before letting Gemini hook in. The solution is to script the log in process through Gemini, and put waits for the log in to succeed, before testing parts of the app that require the authentication. +Authentication presents a tricky problem with Gemini, since we can't inject any cookies or even run a start up script +through the browser before letting Gemini hook in. The solution is to script the log in process through Gemini, and put +waits for the log in to succeed, before testing parts of the app that require the authentication. Browser Session States ---------------------- -Gemini will start a new instance of the browser for each browser configuration defined in the .gemini.yml file when Gemini's launched (ie. `gemini update`, `gemini test`, etc). +Gemini will start a new instance of the browser for each browser configuration defined in the .gemini.yml file when +Gemini's launched (ie. `gemini update`, `gemini test`, etc). -Although each new suite will cause the testing browser to be refreshed, the above means that cookies and other persistent state will be kept across suites for a browser across all runs, even if the suites are from different files. +Although each new suite will cause the testing browser to be refreshed, the above means that cookies and other +persistent state will be kept across suites for a browser across all runs, even if the suites are from different files. -**What this comes down to is**: once you've logged in, you'll stay logged in until you decide to log out or the running instance of Gemini ends. In general practice, it's a good idea to clear the state of the app at the end of each suite of tests by logging out. +**What this comes down to is**: once you've logged in, you'll stay logged in until you decide to log out or the running +instance of Gemini ends. In general practice, it's a good idea to clear the state of the app at the end of each suite of +tests by logging out. -(**Note**: Persistent storage, such as local storage, has not been explicitly tested as to whether they are kept, but as the cookies are cleared each time, this seems unlikely) +(**Note**: Persistent storage, such as local storage, has not been explicitly tested as to whether they are kept, but as +the cookies are cleared each time, this seems unlikely) Test Reporting -------------- -Using the `--report html` flag with Gemini will produce a webpage with the test's results in /gemini-report that will show the old, new, and diff images. Using this is highly recommended (and fun!). +Using the `--reporter html` flag with Gemini will produce a webpage with the test's results in `onion/gemini-report` +that will show the old, new, and diff images. Using this is highly recommended (and fun!) and is used by default in `npm +run vi-test`. +Writing Tests +------------- + +See [the docs](https://github.com/gemini-testing/gemini/blob/master/doc/tests.md), and the [section on the available +actions](https://github.com/gemini-testing/gemini/blob/master/doc/tests.md#available-actions) for what scripted actions +are available. + +Our tests are located in `onion/gemini/`. + +Some useful tips: + * The `find()` method in the callbacks is equivalent to `document.querySelector`; it will only return the first + element found that matches the selector. Use pseudo classes like `nth-of-type()`, `nth-child()`, and etc. to select + later elements. + * Nested suites inherit from their parent suites' configurations, but will **override** their inherited configuration + if another is specified. For example, if `parentSuite` had a `.before()` method, all children of `parentSuite` would + run its `.before()`, but if any of the children specified their own `.before()`, those children would **not** run + `parentSuite`'s `.before()`. + * Gemini takes a screenshot of the minimum bounding rect for all specified selectors, so this means you can't take a + screenshot of two items far away from each other without the rest being considered (ie. trying to get the header and + footer) + * Unfortunately, `setCaptureElements` and `ignoreElements` will only apply for the first element found matching those + selectors. PhantomJS ========= -[PhantomJS](http://phantomjs.org/) is a headless browser that allows us to run tests and take screenshots without needing a browser. +[PhantomJS](http://phantomjs.org/) is a headless browser that allows us to run tests and take screenshots without +needing a browser. -Its second version (PhantomJS2) uses a much more recent version of Webkit, and is a big reason why Gemini (as opposed to other utilities, ie. PhantomCSS) was chosen. Due to the large number of breaking changes introduced between PhantomJS 1.9 to 2.0, a large number of tools (ie. CasperJS) are, at the time of writing, lacking support for 2.0. +Its second version (PhantomJS2) uses a much more recent version of Webkit, and is a big reason why Gemini (as opposed to +other utilities, ie. PhantomCSS) was chosen. Due to the large number of breaking changes introduced between PhantomJS +1.9 to 2.0, a large number of tools (ie. CasperJS) are, at the time of writing, lacking support for 2.0. -While you don't need to know too much about PhantomJS to use and write Gemini tests, there are still a number of useful things to know about. +While you don't need to know too much about PhantomJS to use and write Gemini tests, there are still a number of useful +things to know about. Useful features --------------- @@ -97,28 +146,36 @@ You can find the full list of CLI commands in the [documentation](http://phantom Flags that are of particular interest to us: * `--webdriver=4444`: sets the webdriver port to be 4444, the default webdriver port that Gemini expects. - * `--ignore-ssl-errors=true`: ignores any SSL errors that may occur. Particular useful when hooking up the tests to staging, as the certificate we use is self-signed. + * `--ignore-ssl-errors=true`: ignores any SSL errors that may occur. Particular useful when hooking up the tests to + staging, as the certificate we use is self-signed. * `--ssl-protocol=any`: allows any ssl protocol to be used. May be useful when `--ignore-ssl-errors=true` doesn't work. * '--remote-debugger-port`: allows for remote debugging the running PhantomJS instance. More on this later. Troubleshooting and Debugging ----------------------------- -Remote debugging is possible with PhantomJS using the `--remote-debugger-port` option. See the [troubleshooting docs](http://phantomjs.org/troubleshooting.html). +Remote debugging is possible with PhantomJS using the `--remote-debugger-port` option. See the [troubleshooting +docs](http://phantomjs.org/troubleshooting.html). -To begin using it, add `debugger;` statements to the file being run by `phantomjs`, and access the port number specified after `--remote-debugger-port` on localhost: +To begin using it, add `debugger;` statements to the file being run by `phantomjs`, and access the port number specified +after `--remote-debugger-port` on localhost: ```bash phantomjs --remote-debugger-port=9000 debug.js ``` -PhantomJS will start and then immediately breakpoint. Go to http://localhost:9000/webkit/inspector/inspector.html?page=1 and then to its console tab. Go to your first breakpoint (the first `debugger;` statement executed) by running `__run()` in the console tab. Subsequent breakpoints can be reached by successively running `__run()` in that same console tab. +PhantomJS will start and then immediately breakpoint. Go to http://localhost:9000/webkit/inspector/inspector.html?page=1 +and then to its console tab. Go to your first breakpoint (the first `debugger;` statement executed) by running `__run()` +in the console tab. Subsequent breakpoints can be reached by successively running `__run()` in that same console tab. -At each breakpoint, you can to http://localhost:9000 on a new browser tab and click on one of the links to go to the current execution state of that breakpoint on the page you're on. +At each breakpoint, you can to http://localhost:9000 on a new browser tab and click on one of the links to go to the +current execution state of that breakpoint on the page you're on. --- -To simplify triaging simple issues and test if everything is working, I've added a short test script that can be run with PhantomJS to check if it can access the web app and log in. You can edit the `lauch_app_and_login.js` file to change the environment to run against. +To simplify triaging simple issues and test if everything is working, I've added a short test script that can be run +with PhantomJS to check if it can access the web app and log in. You can edit the `lauch_app_and_login.js` file to +change the environment to run against. ```bash # In root /onion folder From 5cfbe5cc398b38f97b333c88bab0e39e0605d7c0 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Thu, 4 Feb 2016 13:30:16 +0100 Subject: [PATCH 64/94] Rebase remerge --- .../piece_list_toolbar_filter_widget.js | 1 + .../piece_list_toolbar_order_widget.js | 7 ++++--- js/components/header.js | 1 + js/components/header_notification.js | 1 + js/components/nav_routes_links_link.js | 1 + 5 files changed, 8 insertions(+), 3 deletions(-) diff --git a/js/components/ascribe_piece_list_toolbar/piece_list_toolbar_filter_widget.js b/js/components/ascribe_piece_list_toolbar/piece_list_toolbar_filter_widget.js index edb29e85..c9791dbe 100644 --- a/js/components/ascribe_piece_list_toolbar/piece_list_toolbar_filter_widget.js +++ b/js/components/ascribe_piece_list_toolbar/piece_list_toolbar_filter_widget.js @@ -84,6 +84,7 @@ let PieceListToolbarFilterWidget = React.createClass({ if (this.props.filterParams && this.props.filterParams.length) { return ( diff --git a/js/components/ascribe_piece_list_toolbar/piece_list_toolbar_order_widget.js b/js/components/ascribe_piece_list_toolbar/piece_list_toolbar_order_widget.js index 5257cc07..da9bae43 100644 --- a/js/components/ascribe_piece_list_toolbar/piece_list_toolbar_order_widget.js +++ b/js/components/ascribe_piece_list_toolbar/piece_list_toolbar_order_widget.js @@ -45,7 +45,7 @@ let PieceListToolbarOrderWidget = React.createClass({ }, render() { - let filterIcon = ( + let orderIcon = ( · @@ -55,9 +55,10 @@ let PieceListToolbarOrderWidget = React.createClass({ if (this.props.orderParams && this.props.orderParams.length) { return ( + className="ascribe-piece-list-toolbar-filter-widget" + title={orderIcon}>
  • {getLangText('Sort by')}:
  • diff --git a/js/components/header.js b/js/components/header.js index d2ecb76b..2587c92c 100644 --- a/js/components/header.js +++ b/js/components/header.js @@ -173,6 +173,7 @@ let Header = React.createClass({ account = ( diff --git a/js/components/nav_routes_links_link.js b/js/components/nav_routes_links_link.js index 6daec0ea..94fd91a2 100644 --- a/js/components/nav_routes_links_link.js +++ b/js/components/nav_routes_links_link.js @@ -30,6 +30,7 @@ let NavRoutesLinksLink = React.createClass({ return ( {children} From 2661f2ba57db0dfebdeee9dc80a178bf3bcb74f6 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Fri, 29 Jan 2016 11:20:43 +0100 Subject: [PATCH 65/94] Standardize CoaVerify route across apps --- js/components/coa_verify_container.js | 85 +++++++++---------- .../whitelabel/wallet/wallet_routes.js | 10 +-- 2 files changed, 46 insertions(+), 49 deletions(-) diff --git a/js/components/coa_verify_container.js b/js/components/coa_verify_container.js index 6d0af0fa..4a270cf4 100644 --- a/js/components/coa_verify_container.js +++ b/js/components/coa_verify_container.js @@ -27,7 +27,7 @@ let CoaVerifyContainer = React.createClass({ return (
    -
    +
    {getLangText('Verify your Certificate of Authenticity')}
    @@ -37,7 +37,7 @@ let CoaVerifyContainer = React.createClass({ signature={signature}/>

    - {getLangText('ascribe is using the following public key for verification')}: + {getLangText('ascribe is using the following public key for verification')}:
                     -----BEGIN PUBLIC KEY-----
    @@ -60,9 +60,8 @@ let CoaVerifyForm = React.createClass({
         },
     
         handleSuccess(response){
    -        let notification = null;
             if (response.verdict) {
    -            notification = new GlobalNotificationModel(getLangText('Certificate of Authenticity successfully verified'), 'success');
    +            const notification = new GlobalNotificationModel(getLangText('Certificate of Authenticity successfully verified'), 'success');
                 GlobalNotificationActions.appendGlobalNotification(notification);
             }
         },
    @@ -71,46 +70,44 @@ let CoaVerifyForm = React.createClass({
             const { message, signature } = this.props;
     
             return (
    -            
    -
    - {getLangText('Verify your Certificate of Authenticity')} - } - spinner={ - - - - }> - - - - - - -
    -
    -
    +
    + {getLangText('Verify your Certificate of Authenticity')} + + } + spinner={ + + + + }> + + + + + + +
    +
    ); } }); diff --git a/js/components/whitelabel/wallet/wallet_routes.js b/js/components/whitelabel/wallet/wallet_routes.js index ba560608..f1613ee5 100644 --- a/js/components/whitelabel/wallet/wallet_routes.js +++ b/js/components/whitelabel/wallet/wallet_routes.js @@ -78,7 +78,7 @@ let ROUTES = { headerTitle='COLLECTION' disableOn='noPieces' /> - + @@ -114,7 +114,7 @@ let ROUTES = { disableOn='noPieces' /> - + ), @@ -159,7 +159,7 @@ let ROUTES = { component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(IkonotvContractNotifications)} /> - + ), @@ -196,7 +196,7 @@ let ROUTES = { disableOn='noPieces' /> - + ), @@ -233,7 +233,7 @@ let ROUTES = { disableOn='noPieces' /> - + ) From 1d09ef1120bf9f05e00081ab6f8b09fff569b0bb Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Fri, 29 Jan 2016 11:21:57 +0100 Subject: [PATCH 66/94] Main app visual test suite --- .gemini.yml | 15 ++- gemini/main/authenticated.js | 214 +++++++++++++++++++++++++++++++++++ gemini/main/basic.js | 145 ++++++++++++++++++++++++ gemini/main/detail.js | 125 ++++++++++++++++++++ 4 files changed, 493 insertions(+), 6 deletions(-) create mode 100644 gemini/main/authenticated.js create mode 100644 gemini/main/basic.js create mode 100644 gemini/main/detail.js diff --git a/.gemini.yml b/.gemini.yml index 5af1ef27..8d434624 100644 --- a/.gemini.yml +++ b/.gemini.yml @@ -1,14 +1,17 @@ -rootUrl: https://www.ascribe.ninja/app +rootUrl: http://localhost.com:3000/ +sessionsPerBrowser: 1 browsers: - PhantomJSDesktop: - screenshotsDir: './gemini/screens/desktop' + MainDesktop: + rootUrl: http://localhost.com:3000/ + screenshotsDir: './gemini-screens/desktop' windowSize: 1900x1080 desiredCapabilities: browserName: phantomjs - PhantomJSMobile: - screenshotsDir: './gemini/screens/mobile' - windowSize: 767x1364 + MainMobile: + rootUrl: http://localhost.com:3000/ + screenshotsDir: './gemini-screens/mobile' + windowSize: 600x1056 desiredCapabilities: browserName: phantomjs diff --git a/gemini/main/authenticated.js b/gemini/main/authenticated.js new file mode 100644 index 00000000..bf579bff --- /dev/null +++ b/gemini/main/authenticated.js @@ -0,0 +1,214 @@ +'use strict'; + +const gemini = require('gemini'); + +/** + * Suite of tests against routes that require the user to be authenticated. +*/ +gemini.suite('Authenticated', (suite) => { + suite + .setUrl('/collection') + .setCaptureElements('.ascribe-body') + .before((actions, find) => { + // This will be called before every nested suite begins unless that suite + // also defines a `.before()` + // FIXME: use a more generic class for this, like just '.app', + // when we can use this file with the whitelabels + actions.waitForElementToShow('.ascribe-default-app', 5000); + }); + + // Suite just to log us in before any other suites run + gemini.suite('Login', (loginSuite) => { + loginSuite + .setUrl('/login') + .ignoreElements('.ascribe-body') + .capture('logged in', (actions, find) => { + actions.waitForElementToShow('.ascribe-form', 5000); + + actions.sendKeys(find('.ascribe-login-wrapper input[name=email]'), 'dimi@mailinator.com'); + actions.sendKeys(find('.ascribe-login-wrapper input[name=password]'), '0000000000'); + actions.click(find('.ascribe-login-wrapper button[type=submit]')); + + actions.waitForElementToShow('.ascribe-accordion-list:not(.ascribe-loading-position)', 5000); + }); + }); + + gemini.suite('Header-desktop', (headerSuite) => { + headerSuite + .setCaptureElements('nav.navbar .container') + // Ignore Cyland's logo as it's a gif + .ignoreElements('.client--cyland img.img-brand') + .skip(/Mobile/) + .before((actions, find) => { + actions.waitForElementToShow('.ascribe-accordion-list:not(.ascribe-loading-position)', 5000); + }) + .capture('desktop header'); + + gemini.suite('User dropdown', (headerUserSuite) => { + headerUserSuite + .setCaptureElements('#nav-route-user-dropdown ~ .dropdown-menu') + .capture('expanded user dropdown', (actions, find) => { + actions.click(find('#nav-route-user-dropdown')); + }); + }); + + gemini.suite('Notification dropdown', (headerNotificationSuite) => { + headerNotificationSuite + .setCaptureElements('#header-notification-dropdown ~ .dropdown-menu') + .capture('expanded notifications dropdown', (actions, find) => { + actions.click(find('#header-notification-dropdown')); + }); + }); + }); + + // Test for the collapsed header in mobile + gemini.suite('Header-mobile', (headerMobileSuite) => { + headerMobileSuite + .setCaptureElements('nav.navbar .container') + // Ignore Cyland's logo as it's a gif + .ignoreElements('.client--cyland img.img-brand') + .skip(/Desktop/) + .before((actions, find) => { + actions.waitForElementToShow('.ascribe-accordion-list:not(.ascribe-loading-position)', 5000); + }) + .capture('mobile header') + .capture('expanded mobile header', (actions, find) => { + actions.click(find('nav.navbar .navbar-toggle')); + // Wait for the header to expand + actions.wait(500); + }) + .capture('expanded user dropdown', (actions, find) => { + actions.click(find('#nav-route-user-dropdown')); + }) + .capture('expanded notifications dropdown', (actions, find) => { + actions.click(find('#header-notification-dropdown')); + }); + }); + + gemini.suite('Collection', (collectionSuite) => { + collectionSuite + .setCaptureElements('.ascribe-accordion-list') + .before((actions, find) => { + actions.waitForElementToShow('.ascribe-accordion-list:not(.ascribe-loading-position)', 5000); + // Wait for the images to load + // FIXME: unfortuntately gemini doesn't support ignoring multiple elements from a single selector + // so we're forced to wait and hope that the images will all finish loading after 5s. + // We could also change the thumbnails with JS, but setting up a test user is probably easier. + actions.wait(5000); + }) + .capture('collection') + .capture('expanded edition in collection', (actions, find) => { + actions.click(find('.ascribe-accordion-list-item .ascribe-accordion-list-item-edition-widget')); + // Wait for editions to load + actions.waitForElementToShow('.ascribe-accordion-list-item-table', 5000); + }) + + gemini.suite('Collection placeholder', (collectionPlaceholderSuite) => { + collectionPlaceholderSuite + .setCaptureElements('.ascribe-accordion-list-placeholder') + .capture('collection empty search', (actions, find) => { + actions.sendKeys(find('.ascribe-piece-list-toolbar .search-bar input[type="text"]'), 'no search result'); + actions.waitForElementToShow('.ascribe-accordion-list-placeholder', 5000); + }); + }); + + gemini.suite('PieceListBulkModal', (pieceListBulkModalSuite) => { + pieceListBulkModalSuite + .setCaptureElements('.piece-list-bulk-modal') + .capture('items selected', (actions, find) => { + actions.click(find('.ascribe-accordion-list-item .ascribe-accordion-list-item-edition-widget')); + // Wait for editions to load + actions.waitForElementToShow('.ascribe-accordion-list-item-table', 5000); + + actions.click('.ascribe-table thead tr input[type="checkbox"]'); + actions.waitForElementToShow('.piece-list-bulk-modal'); + }); + }); + }); + + gemini.suite('PieceListToolbar', (pieceListToolbarSuite) => { + pieceListToolbarSuite + .setCaptureElements('.ascribe-piece-list-toolbar') + .capture('piece list toolbar') + .capture('piece list toolbar search filled', (actions, find) => { + actions.sendKeys(find('.ascribe-piece-list-toolbar .search-bar input[type="text"]'), 'search text'); + }) + + gemini.suite('Order widget dropdown', (pieceListToolbarOrderWidgetSuite) => { + pieceListToolbarOrderWidgetSuite + .setCaptureElements('#ascribe-piece-list-toolbar-order-widget-dropdown', + '#ascribe-piece-list-toolbar-order-widget-dropdown ~ .dropdown-menu') + .capture('expanded order dropdown', (actions, find) => { + actions.click(find('#ascribe-piece-list-toolbar-order-widget-dropdown')); + + // Wait as the dropdown screenshot still includes the collection in the background + actions.waitForElementToShow('.ascribe-accordion-list:not(.ascribe-loading-position)', 5000); + }); + }); + + gemini.suite('Filter widget dropdown', (pieceListToolbarFilterWidgetSuite) => { + pieceListToolbarFilterWidgetSuite + .setCaptureElements('#ascribe-piece-list-toolbar-filter-widget-dropdown', + '#ascribe-piece-list-toolbar-filter-widget-dropdown ~ .dropdown-menu') + .capture('expanded filter dropdown', (actions, find) => { + actions.click(find('#ascribe-piece-list-toolbar-filter-widget-dropdown')); + + // Wait as the dropdown screenshot still includes the collection in the background + actions.waitForElementToShow('.ascribe-accordion-list:not(.ascribe-loading-position)', 5000); + }); + }); + }); + + gemini.suite('Register work', (registerSuite) => { + registerSuite + .setUrl('/register_piece') + .capture('register work', (actions, find) => { + // The uploader options are only rendered after the user is fetched, so + // we have to wait for it here + actions.waitForElementToShow('.file-drag-and-drop-dialog .present-options', 5000); + }) + .capture('register work filled', (actions, find) => { + actions.sendKeys(find('.ascribe-form input[name="artist_name"]'), 'artist name'); + actions.sendKeys(find('.ascribe-form input[name="title"]'), 'title'); + actions.sendKeys(find('.ascribe-form input[name="date_created"]'), 'date created'); + }) + .capture('register work filled with editions', (actions, find) => { + actions.click(find('.ascribe-form input[type="checkbox"] ~ .checkbox')); + actions.wait(500); + actions.sendKeys(find('.ascribe-form input[name="num_editions"]'), '50'); + }); + + gemini.suite('Register work hash', (registerHashSuite) => { + registerHashSuite + .setUrl('/register_piece?method=hash') + .capture('register work hash method'); + }); + + gemini.suite('Register work upload', (registerUploadSuite) => { + registerUploadSuite + .setUrl('/register_piece?method=upload') + .capture('register work upload method'); + }); + }); + + gemini.suite('User settings', (userSettingsSuite) => { + userSettingsSuite + .setUrl('/settings') + .before((actions, find) => { + // This will be called before every nested suite begins unless that suite + // also defines a `.before()` + actions.waitForElementToShow('.settings-container', 5000); + }) + .capture('user settings'); + }); + + // Suite just to log out after suites have run + gemini.suite('Log out', (logoutSuite) => { + logoutSuite + .setUrl('/logout') + .ignoreElements('.ascribe-body') + .capture('logout', (actions, find) => { + actions.waitForElementToShow('.ascribe-login-wrapper', 10000); + }); + }); +}); diff --git a/gemini/main/basic.js b/gemini/main/basic.js new file mode 100644 index 00000000..317c5d84 --- /dev/null +++ b/gemini/main/basic.js @@ -0,0 +1,145 @@ +'use strict'; + +const gemini = require('gemini'); + +/** + * Basic suite of tests against routes that do not require the user to be authenticated. +*/ +gemini.suite('Basic', (suite) => { + suite + .setUrl('/login') + .setCaptureElements('.ascribe-body') + .before((actions, find) => { + // This will be called before every nested suite begins unless that suite + // also defines a `.before()` + // FIXME: use a more generic class for this, like just '.ascribe-app' + actions.waitForElementToShow('.ascribe-default-app', 5000); + }); + + gemini.suite('Header-desktop', (headerSuite) => { + headerSuite + .setCaptureElements('nav.navbar .container') + .skip(/Mobile/) + .capture('desktop header', (actions, find) => { + actions.waitForElementToShow('nav.navbar .container', 5000); + }) + .capture('hover on active item', (actions, find) => { + const activeItem = find('nav.navbar li.active'); + actions.mouseMove(activeItem); + }) + .capture('hover on inactive item', (actions, find) => { + const inactiveItem = find('nav.navbar li:not(.active)'); + actions.mouseMove(inactiveItem); + }); + }); + + // Test for the collapsed header in mobile + gemini.suite('Header-mobile', (headerMobileSuite) => { + headerMobileSuite + .setCaptureElements('nav.navbar .container') + .skip(/Desktop/) + .capture('mobile header', (actions, find) => { + actions.waitForElementToShow('nav.navbar .container', 5000); + }) + .capture('expanded mobile header', (actions, find) => { + actions.click(find('nav.navbar .navbar-toggle')); + // Wait for the header to expand + actions.wait(500); + }) + .capture('hover on expanded mobile header item', (actions, find) => { + actions.mouseMove(find('nav.navbar li')); + }); + }); + + gemini.suite('Footer', (footerSuite) => { + footerSuite + .setCaptureElements('.ascribe-footer') + .capture('footer', (actions, find) => { + actions.waitForElementToShow('.ascribe-footer', 5000); + }) + .capture('hover on footer item', (actions, find) => { + const footerItem = find('.ascribe-footer a:not(.social)'); + actions.mouseMove(footerItem); + }) + .capture('hover on footer social item', (actions, find) => { + const footerSocialItem = find('.ascribe-footer a.social') + actions.mouseMove(footerSocialItem); + }); + }); + + gemini.suite('Login', (loginSuite) => { + loginSuite + .capture('login', (actions, find) => { + actions.waitForElementToShow('.ascribe-form', 5000); + }) + .capture('hover on login submit', (actions, find) => { + actions.mouseMove(find('.ascribe-form button[type=submit]')); + }) + .capture('hover on sign up link', (actions, find) => { + actions.mouseMove(find('.ascribe-login-text a[href="/signup"]')); + }) + .capture('login form filled with focus', (actions, find) => { + const emailInput = find('.ascribe-form input[name=email]'); + + // Remove hover from sign up link + actions.click(emailInput); + + actions.sendKeys(emailInput, 'dimi@mailinator.com'); + actions.sendKeys(find('.ascribe-form input[name=password]'), '0000000000'); + }) + .capture('login form filled', (actions, find) => { + actions.click(find('.ascribe-form-header')); + }); + }); + + gemini.suite('Sign up', (signUpSuite) => { + signUpSuite + .setUrl('/signup') + .capture('sign up', (actions, find) => { + actions.waitForElementToShow('.ascribe-form', 5000); + }) + .capture('sign up form filled with focus', (actions, find) => { + actions.sendKeys(find('.ascribe-form input[name=email]'), 'dimi@mailinator.com'); + actions.sendKeys(find('.ascribe-form input[name=password]'), '0000000000'); + actions.sendKeys(find('.ascribe-form input[name=password_confirm]'), '0000000000'); + }) + .capture('sign up form filled with check', (actions, find) => { + actions.click(find('.ascribe-form input[type="checkbox"] ~ .checkbox')); + }); + }); + + gemini.suite('Password reset', (passwordResetSuite) => { + passwordResetSuite + .setUrl('/password_reset') + .capture('password reset', (actions, find) => { + actions.waitForElementToShow('.ascribe-form', 5000); + }) + .capture('password reset form filled with focus', (actions, find) => { + actions.sendKeys(find('.ascribe-form input[name="email"]'), 'dimi@mailinator.com'); + }) + .capture('password reset form filled', (actions, find) => { + actions.click(find('.ascribe-form-header')); + }); + }); + + gemini.suite('Coa verify', (coaVerifySuite) => { + coaVerifySuite + .setUrl('/coa_verify') + .capture('coa verify', (actions, find) => { + actions.waitForElementToShow('.ascribe-form', 5000); + }) + .capture('coa verify form filled with focus', (actions, find) => { + actions.sendKeys(find('.ascribe-form input[name="message"]'), 'sample text'); + actions.sendKeys(find('.ascribe-form .ascribe-property-wrapper:nth-of-type(2) textarea'), 'sample signature'); + }) + .capture('coa verify form filled', (actions, find) => { + actions.click(find('.ascribe-login-header')); + }); + }); + + gemini.suite('Not found', (notFoundSuite) => { + notFoundSuite + .setUrl('/not_found_page') + .capture('not found page'); + }); +}); diff --git a/gemini/main/detail.js b/gemini/main/detail.js new file mode 100644 index 00000000..39a02338 --- /dev/null +++ b/gemini/main/detail.js @@ -0,0 +1,125 @@ +'use strict'; + +const gemini = require('gemini'); +const pieceUrl = '/pieces/12374'; +const editionUrl = '/editions/14gw9x3VA9oJaxp4cHaAuK2bvJzvEj4Xvc'; + +/** + * Suite of tests against the piece and edition routes. + * Tests include accessing the piece / edition as the owner or as another user + * (we can just use an anonymous user in this case). +*/ +gemini.suite('Work detail', (suite) => { + suite + .setCaptureElements('.ascribe-body') + .before((actions, find) => { + // This will be called before every nested suite begins unless that suite + // also defines a `.before()` + // FIXME: use a more generic class for this, like just '.app', + // when we can use this file with the whitelabels + actions.waitForElementToShow('.ascribe-default-app', 5000); + + // Wait for the social media buttons to appear + actions.waitForElementToShow('.ascribe-social-button-list .fb-share-button iframe', 20000); + actions.waitForElementToShow('.ascribe-social-button-list .twitter-share-button', 20000); + actions.waitForElementToShow('.ascribe-media-player', 10000); + }); + + gemini.suite('Basic piece', (basicPieceSuite) => { + basicPieceSuite + .setUrl(pieceUrl) + .capture('basic piece') + .capture('shmui', (actions, find) => { + actions.click(find('.ascribe-media-player')); + actions.waitForElementToShow('.shmui-wrap:not(.loading)', 30000); + // Wait for the transition to end + actions.wait(1000); + }); + }); + + gemini.suite('Basic edition', (basicEditionSuite) => { + basicEditionSuite + .setUrl(editionUrl) + .capture('basic edition'); + }); + + // Suite just to log us in before any other suites run + gemini.suite('Login', (loginSuite) => { + loginSuite + .setUrl('/login') + .ignoreElements('.ascribe-body') + .before((actions, find) => { + actions.waitForElementToShow('.ascribe-default-app', 5000); + }) + .capture('logged in', (actions, find) => { + actions.sendKeys(find('.ascribe-login-wrapper input[name=email]'), 'dimi@mailinator.com'); + actions.sendKeys(find('.ascribe-login-wrapper input[name=password]'), '0000000000'); + actions.click(find('.ascribe-login-wrapper button[type=submit]')); + + actions.waitForElementToShow('.ascribe-accordion-list:not(.ascribe-loading-position)', 5000); + }); + }); + + gemini.suite('Authorized piece', (authorizedPieceSuite) => { + authorizedPieceSuite + .setUrl(pieceUrl) + .capture('authorized piece'); + }); + + gemini.suite('Authorized edition', (authorizedEditionSuite) => { + authorizedEditionSuite + .setUrl(editionUrl) + .capture('authorized edition') + }); + + gemini.suite('Detail action buttons', (detailActionButtonSuite) => { + detailActionButtonSuite + .setUrl(editionUrl) + .capture('hover on action button', (actions, find) => { + actions.mouseMove(find('.ascribe-detail-property .ascribe-button-list button.btn-default')); + }) + .capture('hover on delete button', (actions, find) => { + actions.mouseMove(find('.ascribe-detail-property .ascribe-button-list button.btn-tertiary')); + }) + .capture('hover on info button', (actions, find) => { + actions.mouseMove(find('.ascribe-detail-property .ascribe-button-list button.glyphicon-question-sign')); + }) + .capture('expand info text', (actions, find) => { + actions.click(find('.ascribe-detail-property .ascribe-button-list button.glyphicon-question-sign')); + }); + }); + + gemini.suite('Action form modal', (actionFormModalSuite) => { + actionFormModalSuite + .setUrl(editionUrl) + .setCaptureElements('.modal-dialog') + .capture('open email form', (actions, find) => { + // Add class names to make the action buttons easier to select + actions.executeJS(function (window) { + var actionButtons = window.document.querySelectorAll('.ascribe-detail-property .ascribe-button-list button.btn-default'); + for (var ii = 0; ii < actionButtons.length; ++ii) { + if (actionButtons[ii].textContent) { + actionButtons[ii].className += ' ascribe-action-button-' + actionButtons[ii].textContent.toLowerCase(); + } + } + }); + actions.click(find('.ascribe-detail-property .ascribe-button-list button.ascribe-action-button-email')); + + // Wait for transition + actions.wait(1000); + }); + }); + + // Suite just to log out after suites have run + gemini.suite('Log out', (logoutSuite) => { + logoutSuite + .setUrl('/logout') + .ignoreElements('.ascribe-body') + .before((actions, find) => { + actions.waitForElementToShow('.ascribe-default-app', 5000); + }) + .capture('logout', (actions, find) => { + actions.waitForElementToShow('.ascribe-login-wrapper', 10000); + }); + }); +}); From e895c765610e96b72c606d242d8ff55a509494f0 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Fri, 29 Jan 2016 11:24:01 +0100 Subject: [PATCH 67/94] Add whitelabel visual test suite Currently just checks landing page and some pages accessible to unauthorized users. --- .gemini.yml | 116 +++++++++++++++++++ gemini/whitelabel/23vivi/23vivi.js | 27 +++++ gemini/whitelabel/cyland/cyland.js | 28 +++++ gemini/whitelabel/ikonotv/ikonotv.js | 95 +++++++++++++++ gemini/whitelabel/lumenus/lumenus.js | 27 +++++ gemini/whitelabel/shared/whitelabel_basic.js | 112 ++++++++++++++++++ 6 files changed, 405 insertions(+) create mode 100644 gemini/whitelabel/23vivi/23vivi.js create mode 100644 gemini/whitelabel/cyland/cyland.js create mode 100644 gemini/whitelabel/ikonotv/ikonotv.js create mode 100644 gemini/whitelabel/lumenus/lumenus.js create mode 100644 gemini/whitelabel/shared/whitelabel_basic.js diff --git a/.gemini.yml b/.gemini.yml index 8d434624..a04903eb 100644 --- a/.gemini.yml +++ b/.gemini.yml @@ -15,3 +15,119 @@ browsers: windowSize: 600x1056 desiredCapabilities: browserName: phantomjs + + CcDesktop: + rootUrl: http://cc.localhost.com:3000/ + screenshotsDir: './gemini-screens/cc-desktop' + windowSize: 1900x1080 + desiredCapabilities: + browserName: phantomjs + + CcMobile: + rootUrl: http://cc.localhost.com:3000/ + screenshotsDir: './gemini-screens/cc-mobile' + windowSize: 600x1056 + desiredCapabilities: + browserName: phantomjs + + CylandDesktop: + rootUrl: http://cyland.localhost.com:3000/ + screenshotsDir: './gemini-screens/cyland-desktop' + windowSize: 1900x1080 + desiredCapabilities: + browserName: phantomjs + + CylandMobile: + rootUrl: http://cyland.localhost.com:3000/ + screenshotsDir: './gemini-screens/cyland-mobile' + windowSize: 600x1056 + desiredCapabilities: + browserName: phantomjs + + IkonotvDesktop: + rootUrl: http://ikonotv.localhost.com:3000/ + screenshotsDir: './gemini-screens/ikonotv-desktop' + windowSize: 1900x1080 + desiredCapabilities: + browserName: phantomjs + + IkonotvMobile: + rootUrl: http://ikonotv.localhost.com:3000/ + screenshotsDir: './gemini-screens/ikonotv-mobile' + windowSize: 600x1056 + desiredCapabilities: + browserName: phantomjs + + LumenusDesktop: + rootUrl: http://lumenus.localhost.com:3000/ + screenshotsDir: './gemini-screens/lumenus-desktop' + windowSize: 1900x1080 + desiredCapabilities: + browserName: phantomjs + + LumenusMobile: + rootUrl: http://lumenus.localhost.com:3000/ + screenshotsDir: './gemini-screens/lumenus-mobile' + windowSize: 600x1056 + desiredCapabilities: + browserName: phantomjs + + 23viviDesktop: + rootUrl: http://23vivi.localhost.com:3000/ + screenshotsDir: './gemini-screens/23vivi-desktop' + windowSize: 1900x1080 + desiredCapabilities: + browserName: phantomjs + + 23viviMobile: + rootUrl: http://23vivi.localhost.com:3000/ + screenshotsDir: './gemini-screens/23vivi-mobile' + windowSize: 600x1056 + desiredCapabilities: + browserName: phantomjs + +sets: + main: + files: + - gemini/main + browsers: + - MainDesktop + - MainMobile + cc: + files: + - gemini/whitelabel/shared + browsers: + - CcDesktop + - CcMobile + + cyland: + files: + - gemini/whitelabel/shared + - gemini/whitelabel/cyland + browsers: + - CylandDesktop + - CylandMobile + + ikonotv: + files: + - gemini/whitelabel/shared + - gemini/whitelabel/ikonotv + browsers: + - IkonotvDesktop + - IkonotvMobile + + lumenus: + files: + - gemini/whitelabel/shared + - gemini/whitelabel/lumenus + browsers: + - LumenusDesktop + - LumenusMobile + + 23vivi: + files: + - gemini/whitelabel/shared + - gemini/whitelabel/23vivi + browsers: + - 23viviDesktop + - 23viviMobile diff --git a/gemini/whitelabel/23vivi/23vivi.js b/gemini/whitelabel/23vivi/23vivi.js new file mode 100644 index 00000000..cafdfc6d --- /dev/null +++ b/gemini/whitelabel/23vivi/23vivi.js @@ -0,0 +1,27 @@ +'use strict'; + +const gemini = require('gemini'); + +/** + * Suite of tests against 23vivi specific routes + */ +gemini.suite('23vivi', (suite) => { + suite + //TODO: maybe this should be changed to .ascribe-body once the PR that does this is merged + .setCaptureElements('.ascribe-wallet-app') + .before((actions, find) => { + // This will be called before every nested suite begins + actions.waitForElementToShow('.ascribe-wallet-app', 5000); + }); + + gemini.suite('Landing', (landingSuite) => { + landingSuite + .setUrl('/') + .capture('landing', (actions, find) => { + // Wait for the logo to appear + actions.waitForElementToShow('.vivi23-landing--header-logo', 10000); + }); + }); + + // TODO: add more tests for market specific pages after authentication +}); diff --git a/gemini/whitelabel/cyland/cyland.js b/gemini/whitelabel/cyland/cyland.js new file mode 100644 index 00000000..06709f39 --- /dev/null +++ b/gemini/whitelabel/cyland/cyland.js @@ -0,0 +1,28 @@ +'use strict'; + +const gemini = require('gemini'); + +/** + * Suite of tests against Cyland specific routes + */ +gemini.suite('Cyland', (suite) => { + suite + //TODO: maybe this should be changed to .ascribe-body once the PR that does this is merged + .setCaptureElements('.ascribe-wallet-app') + .before((actions, find) => { + // This will be called before every nested suite begins + actions.waitForElementToShow('.ascribe-wallet-app', 5000); + }); + + gemini.suite('Landing', (landingSuite) => { + landingSuite + .setUrl('/') + // Ignore Cyland's logo as it's a gif + .ignoreElements('.cyland-landing img') + .capture('landing', (actions, find) => { + actions.waitForElementToShow('.cyland-landing img', 10000); + }); + }); + + // TODO: add more tests for cyland specific pages after authentication +}); diff --git a/gemini/whitelabel/ikonotv/ikonotv.js b/gemini/whitelabel/ikonotv/ikonotv.js new file mode 100644 index 00000000..1741aaa0 --- /dev/null +++ b/gemini/whitelabel/ikonotv/ikonotv.js @@ -0,0 +1,95 @@ +'use strict'; + +const gemini = require('gemini'); + +/** + * Suite of tests against Cyland specific routes + */ +gemini.suite('Ikonotv', (suite) => { + suite + //TODO: maybe this should be changed to .ascribe-body once the PR that does this is merged + .setCaptureElements('.ascribe-wallet-app') + .before((actions, find) => { + // This will be called before every nested suite begins + actions.waitForElementToShow('.ascribe-wallet-app', 5000); + }); + + gemini.suite('Landing', (landingSuite) => { + landingSuite + .setUrl('/') + // Gemini complains if we try to capture the entire app for Ikonotv's landing page for some reason + .setCaptureElements('.ikonotv-landing') + .setTolerance(5) + .capture('landing', (actions, find) => { + // Stop background animation + actions.executeJS(function (window) { + var landingBackground = window.document.querySelector('.client--ikonotv .route--landing'); + landingBackground.style.animation = 'none'; + landingBackground.style.webkitAnimation = 'none'; + }); + + // Wait for logo to appear + actions.waitForElementToShow('.ikonotv-landing header img', 10000); + }); + }); + + // Ikono needs its own set of tests for some pre-authorization pages to wait for + // its logo to appear + gemini.suite('Ikonotv basic', (suite) => { + suite + .setCaptureElements('.ascribe-wallet-app') + .before((actions, find) => { + // This will be called before every nested suite begins unless that suite + // also defines a `.before()` + // FIXME: use a more generic class for this, like just '.app', + // when we can use this file with the whitelabels + actions.waitForElementToShow('.ascribe-wallet-app', 5000); + + // Wait for the forms to appear + actions.waitForElementToShow('.ascribe-form', 5000); + + // Just use a dumb wait because the logo is set as a background image + actions.wait(3000); + }); + + gemini.suite('Login', (loginSuite) => { + loginSuite + .setUrl('/login') + .capture('login') + .capture('hover on login submit', (actions, find) => { + actions.mouseMove(find('.ascribe-form button[type=submit]')); + }) + .capture('hover on sign up link', (actions, find) => { + actions.mouseMove(find('.ascribe-login-text a[href="/signup"]')); + }) + .capture('login form filled with focus', (actions, find) => { + const emailInput = find('.ascribe-form input[name=email]'); + + // Remove hover from sign up link + actions.click(emailInput); + + actions.sendKeys(emailInput, 'dimi@mailinator.com'); + actions.sendKeys(find('.ascribe-form input[name=password]'), '0000000000'); + }) + .capture('login form filled', (actions, find) => { + actions.click(find('.ascribe-form-header')); + }); + }); + + gemini.suite('Sign up', (signUpSuite) => { + signUpSuite + .setUrl('/signup') + .capture('sign up') + .capture('sign up form filled with focus', (actions, find) => { + actions.sendKeys(find('.ascribe-form input[name=email]'), 'dimi@mailinator.com'); + actions.sendKeys(find('.ascribe-form input[name=password]'), '0000000000'); + actions.sendKeys(find('.ascribe-form input[name=password_confirm]'), '0000000000'); + }) + .capture('sign up form filled with check', (actions, find) => { + actions.click(find('.ascribe-form input[type="checkbox"] ~ .checkbox')); + }); + }); + }); + + // TODO: add more tests for ikonotv specific pages after authentication +}); diff --git a/gemini/whitelabel/lumenus/lumenus.js b/gemini/whitelabel/lumenus/lumenus.js new file mode 100644 index 00000000..a9ff53cd --- /dev/null +++ b/gemini/whitelabel/lumenus/lumenus.js @@ -0,0 +1,27 @@ +'use strict'; + +const gemini = require('gemini'); + +/** + * Suite of tests against lumenus specific routes + */ +gemini.suite('Lumenus', (suite) => { + suite + //TODO: maybe this should be changed to .ascribe-body once the PR that does this is merged + .setCaptureElements('.ascribe-wallet-app') + .before((actions, find) => { + // This will be called before every nested suite begins + actions.waitForElementToShow('.ascribe-wallet-app', 5000); + }); + + gemini.suite('Landing', (landingSuite) => { + landingSuite + .setUrl('/') + .capture('landing', (actions, find) => { + // Wait for the logo to appear + actions.waitForElementToShow('.wp-landing-wrapper img', 10000); + }); + }); + + // TODO: add more tests for market specific pages after authentication +}); diff --git a/gemini/whitelabel/shared/whitelabel_basic.js b/gemini/whitelabel/shared/whitelabel_basic.js new file mode 100644 index 00000000..7fe5c256 --- /dev/null +++ b/gemini/whitelabel/shared/whitelabel_basic.js @@ -0,0 +1,112 @@ +'use strict'; + +const gemini = require('gemini'); + +/** + * Basic suite of tests against whitelabel routes that do not require authentication. +*/ +gemini.suite('Whitelabel basic', (suite) => { + suite + .setCaptureElements('.ascribe-wallet-app > .container') + .before((actions, find) => { + // This will be called before every nested suite begins unless that suite + // also defines a `.before()` + // FIXME: use a more generic class for this, like just '.ascribe-app' + actions.waitForElementToShow('.ascribe-wallet-app', 5000); + + // Use a dumb wait in case we're still waiting for other assets, like fonts, to load + actions.wait(1000); + }); + + gemini.suite('Login', (loginSuite) => { + loginSuite + .setUrl('/login') + // See Ikono + .skip(/Ikono/) + .capture('login', (actions, find) => { + actions.waitForElementToShow('.ascribe-form', 5000); + // For some reason, the screenshots seem to keep catching the whitelabel login form + // on a refresh and without fonts loaded (maybe because they're the first tests run + // and the cache isn't hot yet?). + // Let's wait a bit and hope they load. + actions.wait(3000); + }) + .capture('hover on login submit', (actions, find) => { + actions.mouseMove(find('.ascribe-form button[type=submit]')); + }) + .capture('hover on sign up link', (actions, find) => { + actions.mouseMove(find('.ascribe-login-text a[href="/signup"]')); + }) + .capture('login form filled with focus', (actions, find) => { + const emailInput = find('.ascribe-form input[name=email]'); + + // Remove hover from sign up link + actions.click(emailInput); + + actions.sendKeys(emailInput, 'dimi@mailinator.com'); + actions.sendKeys(find('.ascribe-form input[name=password]'), '0000000000'); + }) + .capture('login form filled', (actions, find) => { + actions.click(find('.ascribe-form-header')); + }); + }); + + gemini.suite('Sign up', (signUpSuite) => { + signUpSuite + .setUrl('/signup') + // See Ikono + .skip(/Ikono/) + .capture('sign up', (actions, find) => { + actions.waitForElementToShow('.ascribe-form', 5000); + // Wait in case the form reloads due to other assets loading + actions.wait(500); + }) + .capture('sign up form filled with focus', (actions, find) => { + actions.sendKeys(find('.ascribe-form input[name=email]'), 'dimi@mailinator.com'); + actions.sendKeys(find('.ascribe-form input[name=password]'), '0000000000'); + actions.sendKeys(find('.ascribe-form input[name=password_confirm]'), '0000000000'); + }) + .capture('sign up form filled with check', (actions, find) => { + actions.click(find('.ascribe-form input[type="checkbox"] ~ .checkbox')); + }); + }); + + gemini.suite('Password reset', (passwordResetSuite) => { + passwordResetSuite + .setUrl('/password_reset') + .capture('password reset', (actions, find) => { + actions.waitForElementToShow('.ascribe-form', 5000); + // Wait in case the form reloads due to other assets loading + actions.wait(500); + }) + .capture('password reset form filled with focus', (actions, find) => { + actions.sendKeys(find('.ascribe-form input[name="email"]'), 'dimi@mailinator.com'); + }) + .capture('password reset form filled', (actions, find) => { + actions.click(find('.ascribe-form-header')); + }); + }); + + gemini.suite('Coa verify', (coaVerifySuite) => { + coaVerifySuite + .setUrl('/coa_verify') + .capture('coa verify', (actions, find) => { + actions.waitForElementToShow('.ascribe-form', 5000); + // Wait in case the form reloads due to other assets loading + actions.wait(500); + }) + .capture('coa verify form filled with focus', (actions, find) => { + actions.sendKeys(find('.ascribe-form input[name="message"]'), 'sample text'); + actions.sendKeys(find('.ascribe-form .ascribe-property-wrapper:nth-of-type(2) textarea'), 'sample signature'); + }) + .capture('coa verify form filled', (actions, find) => { + actions.click(find('.ascribe-login-header')); + }); + }); + + gemini.suite('Not found', (notFoundSuite) => { + notFoundSuite + .setUrl('/not_found_page') + .capture('not found page'); + }); +}); From afb832dc2892030385a3cd93891135aa8949ddba Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Fri, 29 Jan 2016 11:24:28 +0100 Subject: [PATCH 68/94] Remove browser-sync notify pop over that shows up in visual tests --- gulpfile.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gulpfile.js b/gulpfile.js index f13945b0..afa0d5a9 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -97,7 +97,8 @@ gulp.task('browser-sync', function() { proxy: 'http://localhost:4000', port: 3000, open: false, // does not open the browser-window anymore (handled manually) - ghostMode: false + ghostMode: false, + notify: false // stop showing the browsersync pop up }); }); From 747f8df06bbb54f949fe34bac9af263d7b96b136 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Thu, 4 Feb 2016 13:30:55 +0100 Subject: [PATCH 69/94] Rebase remerge --- docs/visual-regression-testing.md | 33 +++++++++++++++++++++++++------ package.json | 15 +++++++++++++- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/docs/visual-regression-testing.md b/docs/visual-regression-testing.md index d544b1fa..35d328f0 100644 --- a/docs/visual-regression-testing.md +++ b/docs/visual-regression-testing.md @@ -42,21 +42,32 @@ Running Tests Run PhantomJS: ```bash -phantomjs --webdriver=4444 +npm run vi-phantom ``` And then run Gemini tests: ```bash -# In root onion/ -gemini test gemini/* --report html +npm run vi-test + +# Run only main tests +npm run vi-test:main + +# Run only whitelabel tests +npm run vi-test:whitelabel + +# Run only specific whitelabel tests +npm run vi-test:cyland ``` -If you've made changes and want them to be the new baseline (ie. it's a correct change--**make sure** to test there are no regressions first!), use +If you've made changes and want them to be the new baseline (ie. it's a correct change--**make sure** to test there are +no regressions first!), use ```bash -# In root onion/ -gemini update gemini/* +npm run vi-update + +# Update just the main app for desktop and mobile +npm run vi-update -- --browser MainDesktop --browser MainMobile ``` @@ -112,6 +123,8 @@ are available. Our tests are located in `onion/gemini/`. +**It would be nice if we kept the whitelabels up to date.** + Some useful tips: * The `find()` method in the callbacks is equivalent to `document.querySelector`; it will only return the first element found that matches the selector. Use pseudo classes like `nth-of-type()`, `nth-child()`, and etc. to select @@ -181,3 +194,11 @@ change the environment to run against. # In root /onion folder phantomjs phantomjs/launch_app_and_login.js ``` + + +TODO +==== + +* Write scripts to automate creation of test users (and modify tests to accomodate) +* Set scripts with rootUrls pointing to staging / live using environment variables +* Set up with Sauce Labs diff --git a/package.json b/package.json index d8e76da5..9b90e537 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,19 @@ "start": "node server.js", "test": "mocha", "tunnel": "node test/tunnel.js" + + "vi-clean": "rm -rf gemini-report", + "vi-phantom": "phantomjs --webdriver=4444", + "vi-update": "gemini update", + "vi-test": "npm run vi-clean && gemini test --reporter html --reporter vflat || true", + "vi-test:all": "npm run vi-test", + "vi-test:main": "npm run vi-test -- --browser MainDesktop --browser MainMobile", + "vi-test:whitelabel": "GEMINI_BROWSERS='CcDesktop, CcMobile, CylandDesktop, CylandMobile, IkonotvDesktop, IkonotvMobile, LumenusDesktop, LumenusMobile, 23viviDesktop, 23viviMobile' npm run vi-test", + "vi-test:cc": "npm run vi-test -- --browser CcDesktop --browser CcMobile", + "vi-test:cyland": "npm run vi-test -- --browser CylandDesktop --browser CylandMobile", + "vi-test:ikonotv": "npm run vi-test -- --browser IkonotvDesktop --browser IkonotvMobile", + "vi-test:lumenus": "npm run vi-test -- --browser LumenusDesktop --browser LumenusMobile", + "vi-test:23vivi": "npm run vi-test -- --browser 23viviDesktop --browser 23viviMobile" }, "browser": { "fineUploader": "./js/components/ascribe_uploader/vendor/s3.fine-uploader.js" @@ -42,7 +55,7 @@ "chai-as-promised": "^5.1.0", "colors": "^1.1.2", "dotenv": "^1.2.0", - "gemini": "^2.0.3", + "gemini": "^2.1.0", "jest-cli": "^0.4.0", "mocha": "^2.3.4", "phantomjs2": "^2.0.2", From c3b8b596f1d51ca7fe368f6bdfe8327ecfbc50da Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Thu, 4 Feb 2016 13:31:16 +0100 Subject: [PATCH 70/94] Rebase remerge --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 9b90e537..bfe0c416 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,9 @@ "postinstall": "npm run build", "build": "gulp build --production", "start": "node server.js", - "test": "mocha", - "tunnel": "node test/tunnel.js" - + "test": "npm run sauce-test", + "sauce-test": "mocha", + "tunnel": "node test/tunnel.js", "vi-clean": "rm -rf gemini-report", "vi-phantom": "phantomjs --webdriver=4444", "vi-update": "gemini update", From a51b33857e4cad8431a5182e11c16b7a765a5db8 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Fri, 29 Jan 2016 13:39:46 +0100 Subject: [PATCH 71/94] Move sauce tests to /test/integration folder --- package.json | 4 ++-- test/{ => integration}/README.md | 0 test/{ => integration}/config.js | 0 test/{ => integration}/setup.js | 0 test/{ => integration/tests}/test-login.js | 2 +- test/{ => integration}/tunnel.js | 0 6 files changed, 3 insertions(+), 3 deletions(-) rename test/{ => integration}/README.md (100%) rename test/{ => integration}/config.js (100%) rename test/{ => integration}/setup.js (100%) rename test/{ => integration/tests}/test-login.js (97%) rename test/{ => integration}/tunnel.js (100%) diff --git a/package.json b/package.json index bfe0c416..42c8bba9 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,8 @@ "build": "gulp build --production", "start": "node server.js", "test": "npm run sauce-test", - "sauce-test": "mocha", - "tunnel": "node test/tunnel.js", + "sauce-test": "mocha ./test/integration/tests/", + "tunnel": "node ./test/integration/tunnel.js", "vi-clean": "rm -rf gemini-report", "vi-phantom": "phantomjs --webdriver=4444", "vi-update": "gemini update", diff --git a/test/README.md b/test/integration/README.md similarity index 100% rename from test/README.md rename to test/integration/README.md diff --git a/test/config.js b/test/integration/config.js similarity index 100% rename from test/config.js rename to test/integration/config.js diff --git a/test/setup.js b/test/integration/setup.js similarity index 100% rename from test/setup.js rename to test/integration/setup.js diff --git a/test/test-login.js b/test/integration/tests/test-login.js similarity index 97% rename from test/test-login.js rename to test/integration/tests/test-login.js index e2736fe1..853d48e5 100644 --- a/test/test-login.js +++ b/test/integration/tests/test-login.js @@ -5,7 +5,7 @@ const wd = require('wd'); const asserters = wd.asserters; // Commonly used asserters for async waits in the browser const chai = require('chai'); const chaiAsPromised = require('chai-as-promised'); -const config = require('./config.js'); +const config = require('../config.js'); chai.use(chaiAsPromised); chai.should(); diff --git a/test/tunnel.js b/test/integration/tunnel.js similarity index 100% rename from test/tunnel.js rename to test/integration/tunnel.js From 67b46fb425e808f67ba41eddf2f458a166eda42d Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Fri, 29 Jan 2016 13:58:52 +0100 Subject: [PATCH 72/94] Move visual tests to /test/gemini folder --- package.json | 6 +++--- .gemini.yml => test/gemini/.gemini.yml | 20 +++++++++---------- .../gemini/README.md | 3 ++- .../gemini/tests}/main/authenticated.js | 0 {gemini => test/gemini/tests}/main/basic.js | 0 {gemini => test/gemini/tests}/main/detail.js | 0 .../gemini/tests}/whitelabel/23vivi/23vivi.js | 0 .../gemini/tests}/whitelabel/cyland/cyland.js | 0 .../tests}/whitelabel/ikonotv/ikonotv.js | 0 .../tests}/whitelabel/lumenus/lumenus.js | 0 .../whitelabel/shared/whitelabel_basic.js | 0 11 files changed, 15 insertions(+), 14 deletions(-) rename .gemini.yml => test/gemini/.gemini.yml (89%) rename docs/visual-regression-testing.md => test/gemini/README.md (97%) rename {gemini => test/gemini/tests}/main/authenticated.js (100%) rename {gemini => test/gemini/tests}/main/basic.js (100%) rename {gemini => test/gemini/tests}/main/detail.js (100%) rename {gemini => test/gemini/tests}/whitelabel/23vivi/23vivi.js (100%) rename {gemini => test/gemini/tests}/whitelabel/cyland/cyland.js (100%) rename {gemini => test/gemini/tests}/whitelabel/ikonotv/ikonotv.js (100%) rename {gemini => test/gemini/tests}/whitelabel/lumenus/lumenus.js (100%) rename {gemini => test/gemini/tests}/whitelabel/shared/whitelabel_basic.js (100%) diff --git a/package.json b/package.json index 42c8bba9..5e72b863 100644 --- a/package.json +++ b/package.json @@ -15,10 +15,10 @@ "test": "npm run sauce-test", "sauce-test": "mocha ./test/integration/tests/", "tunnel": "node ./test/integration/tunnel.js", - "vi-clean": "rm -rf gemini-report", + "vi-clean": "rm -rf ./gemini-report", "vi-phantom": "phantomjs --webdriver=4444", - "vi-update": "gemini update", - "vi-test": "npm run vi-clean && gemini test --reporter html --reporter vflat || true", + "vi-update": "gemini update -c ./test/gemini/.gemini.yml", + "vi-test": "npm run vi-clean && gemini test -c ./test/gemini/.gemini.yml --reporter html --reporter vflat || true", "vi-test:all": "npm run vi-test", "vi-test:main": "npm run vi-test -- --browser MainDesktop --browser MainMobile", "vi-test:whitelabel": "GEMINI_BROWSERS='CcDesktop, CcMobile, CylandDesktop, CylandMobile, IkonotvDesktop, IkonotvMobile, LumenusDesktop, LumenusMobile, 23viviDesktop, 23viviMobile' npm run vi-test", diff --git a/.gemini.yml b/test/gemini/.gemini.yml similarity index 89% rename from .gemini.yml rename to test/gemini/.gemini.yml index a04903eb..f64d865f 100644 --- a/.gemini.yml +++ b/test/gemini/.gemini.yml @@ -89,45 +89,45 @@ browsers: sets: main: files: - - gemini/main + - tests/main browsers: - MainDesktop - MainMobile cc: files: - - gemini/whitelabel/shared + - tests/whitelabel/shared browsers: - CcDesktop - CcMobile cyland: files: - - gemini/whitelabel/shared - - gemini/whitelabel/cyland + - tests/whitelabel/shared + - tests/whitelabel/cyland browsers: - CylandDesktop - CylandMobile ikonotv: files: - - gemini/whitelabel/shared - - gemini/whitelabel/ikonotv + - tests/whitelabel/shared + - tests/whitelabel/ikonotv browsers: - IkonotvDesktop - IkonotvMobile lumenus: files: - - gemini/whitelabel/shared - - gemini/whitelabel/lumenus + - tests/whitelabel/shared + - tests/whitelabel/lumenus browsers: - LumenusDesktop - LumenusMobile 23vivi: files: - - gemini/whitelabel/shared - - gemini/whitelabel/23vivi + - tests/whitelabel/shared + - tests/whitelabel/23vivi browsers: - 23viviDesktop - 23viviMobile diff --git a/docs/visual-regression-testing.md b/test/gemini/README.md similarity index 97% rename from docs/visual-regression-testing.md rename to test/gemini/README.md index 35d328f0..da1712d9 100644 --- a/docs/visual-regression-testing.md +++ b/test/gemini/README.md @@ -24,6 +24,7 @@ Then, install [PhantomJS2](https://www.npmjs.com/package/phantomjs2): ```bash # Until phantomjs2 is updated for the new 2.1 version of PhantomJS, use the following (go to https://bitbucket.org/ariya/phantomjs/downloads to find a build for your OS) npm install -g phantomjs2 --phantomjs_downloadurl=https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-macosx.zip +npm install --save-dev phantomjs2 --phantomjs_downloadurl=https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-macosx.zip # If using OSX, you may have to install upx and decompress the binary downloaded by npm manually: brew install upx @@ -121,7 +122,7 @@ See [the docs](https://github.com/gemini-testing/gemini/blob/master/doc/tests.md actions](https://github.com/gemini-testing/gemini/blob/master/doc/tests.md#available-actions) for what scripted actions are available. -Our tests are located in `onion/gemini/`. +Our tests are located in `onion/test/gemini/tests/`. **It would be nice if we kept the whitelabels up to date.** diff --git a/gemini/main/authenticated.js b/test/gemini/tests/main/authenticated.js similarity index 100% rename from gemini/main/authenticated.js rename to test/gemini/tests/main/authenticated.js diff --git a/gemini/main/basic.js b/test/gemini/tests/main/basic.js similarity index 100% rename from gemini/main/basic.js rename to test/gemini/tests/main/basic.js diff --git a/gemini/main/detail.js b/test/gemini/tests/main/detail.js similarity index 100% rename from gemini/main/detail.js rename to test/gemini/tests/main/detail.js diff --git a/gemini/whitelabel/23vivi/23vivi.js b/test/gemini/tests/whitelabel/23vivi/23vivi.js similarity index 100% rename from gemini/whitelabel/23vivi/23vivi.js rename to test/gemini/tests/whitelabel/23vivi/23vivi.js diff --git a/gemini/whitelabel/cyland/cyland.js b/test/gemini/tests/whitelabel/cyland/cyland.js similarity index 100% rename from gemini/whitelabel/cyland/cyland.js rename to test/gemini/tests/whitelabel/cyland/cyland.js diff --git a/gemini/whitelabel/ikonotv/ikonotv.js b/test/gemini/tests/whitelabel/ikonotv/ikonotv.js similarity index 100% rename from gemini/whitelabel/ikonotv/ikonotv.js rename to test/gemini/tests/whitelabel/ikonotv/ikonotv.js diff --git a/gemini/whitelabel/lumenus/lumenus.js b/test/gemini/tests/whitelabel/lumenus/lumenus.js similarity index 100% rename from gemini/whitelabel/lumenus/lumenus.js rename to test/gemini/tests/whitelabel/lumenus/lumenus.js diff --git a/gemini/whitelabel/shared/whitelabel_basic.js b/test/gemini/tests/whitelabel/shared/whitelabel_basic.js similarity index 100% rename from gemini/whitelabel/shared/whitelabel_basic.js rename to test/gemini/tests/whitelabel/shared/whitelabel_basic.js From 574378af98d5fbc3f78013332c006b3c52612117 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Fri, 29 Jan 2016 13:58:57 +0100 Subject: [PATCH 73/94] Update README for updated paths to tests --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1b918e5d..8dcf78ad 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,14 @@ Visual Regression Testing We're using [Gemini](https://github.com/gemini-testing/gemini) for visual regression tests because it supports both PhantomJS2 and SauceLabs. -See the [helper docs](docs/visual-regression-testing.md) for information on installing Gemini, its dependencies, and running and writing tests. +See the [helper docs](test/gemini/README.md) for information on installing Gemini, its dependencies, and running and writing tests. + +Integration Testing +------------------- + +We're using [Sauce Labs](https://saucelabs.com/home) with [WD.js](https://github.com/admc/wd) for integration testing across browser grids with Selenium. + +See the [helper docs](test/integration/README.md) for information on each part of the test stack and how to run and write tests. Workflow From baf75cbee12912f712598417a7b90caddbcadfbc Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Fri, 29 Jan 2016 14:37:41 +0100 Subject: [PATCH 74/94] Fix shmui visual test --- test/gemini/tests/main/detail.js | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/test/gemini/tests/main/detail.js b/test/gemini/tests/main/detail.js index 39a02338..7adad5d4 100644 --- a/test/gemini/tests/main/detail.js +++ b/test/gemini/tests/main/detail.js @@ -29,12 +29,17 @@ gemini.suite('Work detail', (suite) => { basicPieceSuite .setUrl(pieceUrl) .capture('basic piece') - .capture('shmui', (actions, find) => { - actions.click(find('.ascribe-media-player')); - actions.waitForElementToShow('.shmui-wrap:not(.loading)', 30000); - // Wait for the transition to end - actions.wait(1000); - }); + + gemini.suite('Shmui', (shmuiSuite) => { + shmuiSuite. + setCaptureElements('.shmui-wrap') + .capture('shmui', (actions, find) => { + actions.click(find('.ascribe-media-player')); + actions.waitForElementToShow('.shmui-wrap:not(.loading)', 30000); + // Wait for the transition to end + actions.wait(1000); + }); + }); }); gemini.suite('Basic edition', (basicEditionSuite) => { From 39417f3d11a18c3bdbc0a6458994dbb113142539 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Fri, 29 Jan 2016 14:37:55 +0100 Subject: [PATCH 75/94] Change screenshot folder for visual tests --- test/gemini/.gemini.yml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/test/gemini/.gemini.yml b/test/gemini/.gemini.yml index f64d865f..0c7b8b77 100644 --- a/test/gemini/.gemini.yml +++ b/test/gemini/.gemini.yml @@ -4,84 +4,84 @@ sessionsPerBrowser: 1 browsers: MainDesktop: rootUrl: http://localhost.com:3000/ - screenshotsDir: './gemini-screens/desktop' + screenshotsDir: './screenshots/main-desktop' windowSize: 1900x1080 desiredCapabilities: browserName: phantomjs MainMobile: rootUrl: http://localhost.com:3000/ - screenshotsDir: './gemini-screens/mobile' + screenshotsDir: './screenshots/main-mobile' windowSize: 600x1056 desiredCapabilities: browserName: phantomjs CcDesktop: rootUrl: http://cc.localhost.com:3000/ - screenshotsDir: './gemini-screens/cc-desktop' + screenshotsDir: './screenshots/cc-desktop' windowSize: 1900x1080 desiredCapabilities: browserName: phantomjs CcMobile: rootUrl: http://cc.localhost.com:3000/ - screenshotsDir: './gemini-screens/cc-mobile' + screenshotsDir: './screenshots/cc-mobile' windowSize: 600x1056 desiredCapabilities: browserName: phantomjs CylandDesktop: rootUrl: http://cyland.localhost.com:3000/ - screenshotsDir: './gemini-screens/cyland-desktop' + screenshotsDir: './screenshots/cyland-desktop' windowSize: 1900x1080 desiredCapabilities: browserName: phantomjs CylandMobile: rootUrl: http://cyland.localhost.com:3000/ - screenshotsDir: './gemini-screens/cyland-mobile' + screenshotsDir: './screenshots/cyland-mobile' windowSize: 600x1056 desiredCapabilities: browserName: phantomjs IkonotvDesktop: rootUrl: http://ikonotv.localhost.com:3000/ - screenshotsDir: './gemini-screens/ikonotv-desktop' + screenshotsDir: './screenshots/ikonotv-desktop' windowSize: 1900x1080 desiredCapabilities: browserName: phantomjs IkonotvMobile: rootUrl: http://ikonotv.localhost.com:3000/ - screenshotsDir: './gemini-screens/ikonotv-mobile' + screenshotsDir: './screenshots/ikonotv-mobile' windowSize: 600x1056 desiredCapabilities: browserName: phantomjs LumenusDesktop: rootUrl: http://lumenus.localhost.com:3000/ - screenshotsDir: './gemini-screens/lumenus-desktop' + screenshotsDir: './screenshots/lumenus-desktop' windowSize: 1900x1080 desiredCapabilities: browserName: phantomjs LumenusMobile: rootUrl: http://lumenus.localhost.com:3000/ - screenshotsDir: './gemini-screens/lumenus-mobile' + screenshotsDir: './screenshots/lumenus-mobile' windowSize: 600x1056 desiredCapabilities: browserName: phantomjs 23viviDesktop: rootUrl: http://23vivi.localhost.com:3000/ - screenshotsDir: './gemini-screens/23vivi-desktop' + screenshotsDir: './screenshots/23vivi-desktop' windowSize: 1900x1080 desiredCapabilities: browserName: phantomjs 23viviMobile: rootUrl: http://23vivi.localhost.com:3000/ - screenshotsDir: './gemini-screens/23vivi-mobile' + screenshotsDir: './screenshots/23vivi-mobile' windowSize: 600x1056 desiredCapabilities: browserName: phantomjs From 4e13376d5ff993a58968757426b1a155fb4beaed Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Mon, 1 Feb 2016 14:52:05 +0100 Subject: [PATCH 76/94] Add route class names to prize app --- js/components/whitelabel/prize/portfolioreview/pr_app.js | 7 ++++++- js/components/whitelabel/prize/simple_prize/prize_app.js | 5 +++-- js/components/whitelabel/wallet/wallet_app.js | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/js/components/whitelabel/prize/portfolioreview/pr_app.js b/js/components/whitelabel/prize/portfolioreview/pr_app.js index 9637c167..7e21d7c3 100644 --- a/js/components/whitelabel/prize/portfolioreview/pr_app.js +++ b/js/components/whitelabel/prize/portfolioreview/pr_app.js @@ -1,6 +1,7 @@ 'use strict'; import React from 'react'; +import classNames from 'classnames'; import EventActions from '../../../../actions/event_actions'; @@ -63,6 +64,10 @@ let PRApp = React.createClass({ const { currentUser } = this.state; const subdomain = getSubdomain(); + // The second element of routes is always the active component object, where we can + // extract the path. + const path = routes[1] ? routes[1].path : null; + let style = {}; let header; if (currentUser && currentUser.email && history.isActive(`/pieces/${getCookie(currentUser.email)}`)) { @@ -77,7 +82,7 @@ let PRApp = React.createClass({ return (
    + className={classNames('ascribe-prize-app', `route--${(path ? path.split('/')[0] : 'landing')}`)}> {header}
    {/* Routes are injected here */} diff --git a/js/components/whitelabel/prize/simple_prize/prize_app.js b/js/components/whitelabel/prize/simple_prize/prize_app.js index e40e8dc8..da02a597 100644 --- a/js/components/whitelabel/prize/simple_prize/prize_app.js +++ b/js/components/whitelabel/prize/simple_prize/prize_app.js @@ -1,6 +1,7 @@ 'use strict'; import React from 'react'; +import classNames from 'classnames'; import Hero from './components/prize_hero'; @@ -27,7 +28,7 @@ let PrizeApp = React.createClass({ // The second element of routes is always the active component object, where we can // extract the path. - let path = routes[1] ? routes[1].path : null; + const path = routes[1] ? routes[1].path : null; let header = null; // if the path of the current activeRoute is not defined, then this is the IndexRoute @@ -38,7 +39,7 @@ let PrizeApp = React.createClass({ } return ( -
    +
    {header}
    {/* Routes are injected here */} diff --git a/js/components/whitelabel/wallet/wallet_app.js b/js/components/whitelabel/wallet/wallet_app.js index 4735bdde..3940d89e 100644 --- a/js/components/whitelabel/wallet/wallet_app.js +++ b/js/components/whitelabel/wallet/wallet_app.js @@ -26,7 +26,7 @@ let WalletApp = React.createClass({ // The second element of routes is always the active component object, where we can // extract the path. - let path = routes[1] ? routes[1].path : null; + const path = routes[1] ? routes[1].path : null; let header = null; // if the path of the current activeRoute is not defined, then this is the IndexRoute From 83012200d168d7551e7b6eb51b0bc1d5f13171b8 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Mon, 1 Feb 2016 14:53:54 +0100 Subject: [PATCH 77/94] Separate Sluice styling away from SimplePrize styling --- sass/whitelabel/prize/index.scss | 3 +- .../portfolioreview_custom_style.scss | 2 +- .../simple_prize_custom_style.scss | 189 +----------------- .../simple_prize/simple_prize_variables.scss | 3 + .../prize/sluice/sluice_custom_style.scss | 182 +++++++++++++++++ 5 files changed, 190 insertions(+), 189 deletions(-) create mode 100644 sass/whitelabel/prize/simple_prize/simple_prize_variables.scss create mode 100644 sass/whitelabel/prize/sluice/sluice_custom_style.scss diff --git a/sass/whitelabel/prize/index.scss b/sass/whitelabel/prize/index.scss index dfdcaebd..664fe7a1 100644 --- a/sass/whitelabel/prize/index.scss +++ b/sass/whitelabel/prize/index.scss @@ -1,9 +1,10 @@ +@import 'simple_prize/simple_prize_variables'; @import 'simple_prize/simple_prize_custom_style'; +@import 'sluice/sluice_custom_style'; @import 'portfolioreview/portfolioreview_custom_style'; .ascribe-prize-app { border-radius: 0; - min-height: 100vh; padding-top: 70px; padding-bottom: 10px; } diff --git a/sass/whitelabel/prize/portfolioreview/portfolioreview_custom_style.scss b/sass/whitelabel/prize/portfolioreview/portfolioreview_custom_style.scss index 0759cf49..b0027bff 100644 --- a/sass/whitelabel/prize/portfolioreview/portfolioreview_custom_style.scss +++ b/sass/whitelabel/prize/portfolioreview/portfolioreview_custom_style.scss @@ -90,7 +90,7 @@ $pr--button-color: $pr--nav-fg-prim-color; .register-piece--info { text-align: center; - + h1, h2 { font-variant: small-caps; } diff --git a/sass/whitelabel/prize/simple_prize/simple_prize_custom_style.scss b/sass/whitelabel/prize/simple_prize/simple_prize_custom_style.scss index 4cfb7c82..8c46d366 100644 --- a/sass/whitelabel/prize/simple_prize/simple_prize_custom_style.scss +++ b/sass/whitelabel/prize/simple_prize/simple_prize_custom_style.scss @@ -1,7 +1,3 @@ -$sluice--nav-bg-color: #fcfcfc; -$sluice--nav-fg-prim-color: #1E1E1E; -$sluice--button-color: $sluice--nav-fg-prim-color; - .wp { height: 100%; max-width: 90%; @@ -37,10 +33,10 @@ $sluice--button-color: $sluice--nav-fg-prim-color; .rating-container { - color: lighten($sluice--nav-fg-prim-color, 80%) !important; + color: lighten($simple-prize--nav-fg-prim-color, 80%) !important; .rating-stars { width: 25px; - color: $sluice--nav-fg-prim-color !important; + color: $simple-prize--nav-fg-prim-color !important; } } @@ -73,185 +69,4 @@ $sluice--button-color: $sluice--nav-fg-prim-color; } -.client--sluice { - .navbar-default { - background-color: $sluice--nav-bg-color; - box-shadow: none; - border-bottom: 1px solid rgba(0, 0, 0, 0.05); - } - .navbar-nav > li > a, - .navbar-nav > li > .active a { - color: $sluice--nav-fg-prim-color; - background-color: $sluice--nav-bg-color; - } - .navbar-nav > li > a:hover { - color: lighten($sluice--nav-fg-prim-color, 40%); - } - .navbar-nav > .active a, - .navbar-nav > .active a:hover, - .navbar-nav > .active a:focus { - color: $sluice--nav-fg-prim-color; - border-bottom-color: $sluice--nav-fg-prim-color; - background-color: $sluice--nav-bg-color; - } - .dropdown-menu > li > a:hover, - .dropdown-menu > li > a:focus { - color: lighten($sluice--nav-fg-prim-color, 40%); - background-color: $sluice--nav-bg-color; - } - .navbar-nav > .open > a, - .navbar-nav > .open > a:hover, - .navbar-nav > .open > a:focus, - .dropdown-menu > .active > a, - .dropdown-menu > .active > a:hover, - .dropdown-menu > .active > a:focus { - color: lighten($sluice--nav-fg-prim-color, 40%); - background-color: $sluice--nav-bg-color; - } - .dropdown-menu { - background-color: $sluice--nav-bg-color; - } - .dropdown-menu > li > a { - color: $sluice--nav-fg-prim-color; - } - - .navbar-toggle .icon-bar { - background-color: $sluice--nav-fg-prim-color; - } - - .navbar-toggle:hover, - .navbar-toggle:focus { - background-color: $sluice--nav-bg-color; - } -} - -.client--sluice .ascribe-footer { - display: none; -} - - -.client--sluice .icon-ascribe-search{ - color: $sluice--button-color; -} - -.client--sluice .ascribe-piece-list-toolbar .btn-ascribe-add{ - display: none; -} - -// disabled buttons -.client--sluice { - .btn-default.disabled, - .btn-default.disabled:hover, - .btn-default.disabled:focus, - .btn-default.disabled.focus, - .btn-default.disabled:active, - .btn-default.disabled.active, - .btn-default[disabled], - .btn-default[disabled]:hover, - .btn-default[disabled]:focus, - .btn-default[disabled].focus, - .btn-default[disabled]:active, - .btn-default[disabled].active, - fieldset[disabled] .btn-default, - fieldset[disabled] .btn-default:hover, - fieldset[disabled] .btn-default:focus, - fieldset[disabled] .btn-default.focus, - fieldset[disabled] .btn-default:active, - fieldset[disabled] .btn-default.active { - background-color: darken($sluice--button-color, 20%); - border-color: darken($sluice--button-color, 20%); - } -} - -// buttons! -// thought of the day: -// "every great atrocity is the result of people just following orders" -.client--sluice { - .ascribe-piece-list-toolbar-filter-widget button { - color: $sluice--button-color !important; - background-color: transparent !important; - border-color: transparent !important; - - &:hover, - &:active { - background-color: $sluice--button-color !important; - border-color: $sluice--button-color !important; - color: white !important; - } - } - - .btn-wide, - .btn-default { - background-color: $sluice--button-color; - border-color: $sluice--button-color; - - &:hover, - &:active, - &:focus, - &:active:hover, - &:active:focus, - &:active.focus, - &.active:hover, - &.active:focus, - &.active.focus { - background-color: lighten($sluice--button-color, 20%); - border-color: lighten($sluice--button-color, 20%); - } - } - - .open > .btn-default.dropdown-toggle:hover, - .open > .btn-default.dropdown-toggle:focus, - .open > .btn-default.dropdown-toggle.focus, - .open > .btn-default.dropdown-toggle.dropdown-toggle { - background-color: darken($sluice--button-color, 20%); - border-color: darken($sluice--button-color, 20%); - } - - .pager li > a, .pager li > span { - background-color: $sluice--button-color; - border-color: $sluice--button-color; - } - - .pager li.disabled > a, - .pager li.disabled > span { - background-color: $sluice--button-color !important; - border-color: $sluice--button-color; - } -} - -// spinner! -.client--sluice { - .btn-spinner { - color: $sluice--button-color; - } - .spinner-circle { - border-color: $sluice--button-color; - } - .spinner-inner { - color: $sluice--button-color; - display: none; - } -} - -// intercom stuff -.client--sluice { - #intercom-container .intercom-launcher-button { - background-color: $sluice--button-color !important;; - border-color: $sluice--button-color !important;; - } -} - -// notifications -.client--sluice .ascribe-global-notification-success { - background-color: lighten($sluice--button-color, 50%); -} - -// progress bar -.client--sluice .ascribe-progress-bar > .progress-bar { - background-color: $sluice--button-color; -} - -.client--sluice .acl-information-dropdown-list .title { - color: $sluice--button-color; -} \ No newline at end of file diff --git a/sass/whitelabel/prize/simple_prize/simple_prize_variables.scss b/sass/whitelabel/prize/simple_prize/simple_prize_variables.scss new file mode 100644 index 00000000..64ec5c9c --- /dev/null +++ b/sass/whitelabel/prize/simple_prize/simple_prize_variables.scss @@ -0,0 +1,3 @@ +$simple-prize--nav-bg-color: #fcfcfc; +$simple-prize--nav-fg-prim-color: #1E1E1E; +$simple-prize--button-color: $simple-prize--nav-fg-prim-color; diff --git a/sass/whitelabel/prize/sluice/sluice_custom_style.scss b/sass/whitelabel/prize/sluice/sluice_custom_style.scss new file mode 100644 index 00000000..bcc4978c --- /dev/null +++ b/sass/whitelabel/prize/sluice/sluice_custom_style.scss @@ -0,0 +1,182 @@ +.client--sluice { + .navbar-default { + background-color: $simple-prize--nav-bg-color; + box-shadow: none; + border-bottom: 1px solid rgba(0, 0, 0, 0.05); + } + .navbar-nav > li > a, + .navbar-nav > li > .active a { + color: $simple-prize--nav-fg-prim-color; + background-color: $simple-prize--nav-bg-color; + } + .navbar-nav > li > a:hover { + color: lighten($simple-prize--nav-fg-prim-color, 40%); + } + .navbar-nav > .active a, + .navbar-nav > .active a:hover, + .navbar-nav > .active a:focus { + color: $simple-prize--nav-fg-prim-color; + border-bottom-color: $simple-prize--nav-fg-prim-color; + background-color: $simple-prize--nav-bg-color; + } + .dropdown-menu > li > a:hover, + .dropdown-menu > li > a:focus { + color: lighten($simple-prize--nav-fg-prim-color, 40%); + background-color: $simple-prize--nav-bg-color; + } + .navbar-nav > .open > a, + .navbar-nav > .open > a:hover, + .navbar-nav > .open > a:focus, + .dropdown-menu > .active > a, + .dropdown-menu > .active > a:hover, + .dropdown-menu > .active > a:focus { + color: lighten($simple-prize--nav-fg-prim-color, 40%); + background-color: $simple-prize--nav-bg-color; + } + .dropdown-menu { + background-color: $simple-prize--nav-bg-color; + } + + .dropdown-menu > li > a { + color: $simple-prize--nav-fg-prim-color; + } + + .navbar-toggle .icon-bar { + background-color: $simple-prize--nav-fg-prim-color; + } + + .navbar-toggle:hover, + .navbar-toggle:focus { + background-color: $simple-prize--nav-bg-color; + } +} + +.client--sluice .ascribe-footer { + display: none; +} + + +.client--sluice .icon-ascribe-search{ + color: $simple-prize--button-color; +} + +.client--sluice .ascribe-piece-list-toolbar .btn-ascribe-add{ + display: none; +} + +// disabled buttons +.client--sluice { + .btn-default.disabled, + .btn-default.disabled:hover, + .btn-default.disabled:focus, + .btn-default.disabled.focus, + .btn-default.disabled:active, + .btn-default.disabled.active, + .btn-default[disabled], + .btn-default[disabled]:hover, + .btn-default[disabled]:focus, + .btn-default[disabled].focus, + .btn-default[disabled]:active, + .btn-default[disabled].active, + fieldset[disabled] .btn-default, + fieldset[disabled] .btn-default:hover, + fieldset[disabled] .btn-default:focus, + fieldset[disabled] .btn-default.focus, + fieldset[disabled] .btn-default:active, + fieldset[disabled] .btn-default.active { + background-color: darken($simple-prize--button-color, 20%); + border-color: darken($simple-prize--button-color, 20%); + } +} + +// buttons! +// thought of the day: +// "every great atrocity is the result of people just following orders" +.client--sluice { + .ascribe-piece-list-toolbar-filter-widget button { + color: $simple-prize--button-color !important; + background-color: transparent !important; + border-color: transparent !important; + + &:hover, + &:active { + background-color: $simple-prize--button-color !important; + border-color: $simple-prize--button-color !important; + color: white !important; + } + } + + .btn-wide, + .btn-default { + background-color: $simple-prize--button-color; + border-color: $simple-prize--button-color; + + &:hover, + &:active, + &:focus, + &:active:hover, + &:active:focus, + &:active.focus, + &.active:hover, + &.active:focus, + &.active.focus { + background-color: lighten($simple-prize--button-color, 20%); + border-color: lighten($simple-prize--button-color, 20%); + } + } + + .open > .btn-default.dropdown-toggle:hover, + .open > .btn-default.dropdown-toggle:focus, + .open > .btn-default.dropdown-toggle.focus, + .open > .btn-default.dropdown-toggle.dropdown-toggle { + background-color: darken($simple-prize--button-color, 20%); + border-color: darken($simple-prize--button-color, 20%); + } + + .pager li > a, .pager li > span { + background-color: $simple-prize--button-color; + border-color: $simple-prize--button-color; + } + + .pager li.disabled > a, + .pager li.disabled > span { + background-color: $simple-prize--button-color !important; + border-color: $simple-prize--button-color; + } +} + +// spinner! +.client--sluice { + .btn-spinner { + color: $simple-prize--button-color; + } + .spinner-circle { + border-color: $simple-prize--button-color; + } + .spinner-inner { + color: $simple-prize--button-color; + display: none; + } +} + +// intercom stuff +.client--sluice { + #intercom-container .intercom-launcher-button { + background-color: $simple-prize--button-color !important;; + border-color: $simple-prize--button-color !important;; + } +} + +// notifications +.client--sluice .ascribe-global-notification-success { + background-color: lighten($simple-prize--button-color, 50%); +} + +// progress bar +.client--sluice .ascribe-progress-bar > .progress-bar { + background-color: $simple-prize--button-color; +} + +.client--sluice .acl-information-dropdown-list .title { + color: $simple-prize--button-color; +} From 764f81925be4128e25ac29f5501a80edb0bcc1e5 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Mon, 1 Feb 2016 14:48:44 +0100 Subject: [PATCH 78/94] Change `children` prop from react-router to only be a Element type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Judging by https://github.com/rackt/react-router/blob/master/docs/API.md#children-1 and a few inspections in the code, as well as tests, the `children` prop injected into routes can only ever be a single React Element object. This allows us to easily get the active route of a child (if there is an active route) by querying the `children`’s route prop. --- js/components/app_base.js | 20 ++++++++++++------- js/components/ascribe_app.js | 11 ++++------ .../prize/portfolioreview/pr_app.js | 11 ++++------ .../prize/simple_prize/prize_app.js | 11 ++++------ js/components/whitelabel/wallet/wallet_app.js | 11 ++++------ js/routes.js | 10 +++++----- 6 files changed, 34 insertions(+), 40 deletions(-) diff --git a/js/components/app_base.js b/js/components/app_base.js index 044b984c..4b5d460e 100644 --- a/js/components/app_base.js +++ b/js/components/app_base.js @@ -15,14 +15,10 @@ export default function AppBase(App) { displayName: 'AppBase', propTypes: { + children: React.PropTypes.element.isRequired, history: React.PropTypes.object.isRequired, location: React.PropTypes.object.isRequired, - routes: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, - - children: React.PropTypes.oneOfType([ - React.PropTypes.arrayOf(React.PropTypes.element), - React.PropTypes.element - ]) + routes: React.PropTypes.arrayOf(React.PropTypes.object).isRequired }, mixins: [History], @@ -42,10 +38,20 @@ export default function AppBase(App) { }, render() { + const { children } = this.props; + + // Get the currently active route of the app by using the injected route parameter + // on the currently active child route. + // Note that despite its name, this.props.children can only ever be a single + // React.PropTypes.element. + const activeRoute = children.props.route; + return (
    -
    + diff --git a/js/components/ascribe_app.js b/js/components/ascribe_app.js index 8999246e..0e28aa97 100644 --- a/js/components/ascribe_app.js +++ b/js/components/ascribe_app.js @@ -8,16 +8,13 @@ import Header from './header'; let AscribeApp = React.createClass({ propTypes: { - routes: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, - - children: React.PropTypes.oneOfType([ - React.PropTypes.arrayOf(React.PropTypes.element), - React.PropTypes.element - ]) + activeRoute: React.PropTypes.object.isRequired, + children: React.PropTypes.element.isRequired, + routes: React.PropTypes.arrayOf(React.PropTypes.object).isRequired }, render() { - const { children, routes } = this.props; + const { activeRoute, children, routes } = this.props; return (
    diff --git a/js/components/whitelabel/prize/portfolioreview/pr_app.js b/js/components/whitelabel/prize/portfolioreview/pr_app.js index 7e21d7c3..04b985db 100644 --- a/js/components/whitelabel/prize/portfolioreview/pr_app.js +++ b/js/components/whitelabel/prize/portfolioreview/pr_app.js @@ -19,13 +19,10 @@ import { getCookie } from '../../../../utils/fetch_api_utils'; let PRApp = React.createClass({ propTypes: { + activeRoute: React.PropTypes.object.isRequired, + children: React.PropTypes.element.isRequired, history: React.PropTypes.object.isRequired, - routes: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, - - children: React.PropTypes.oneOfType([ - React.PropTypes.arrayOf(React.PropTypes.element), - React.PropTypes.element - ]) + routes: React.PropTypes.arrayOf(React.PropTypes.object).isRequired }, getInitialState() { @@ -60,7 +57,7 @@ let PRApp = React.createClass({ render() { - const { children, history, routes } = this.props; + const { activeRoute, children, history, routes } = this.props; const { currentUser } = this.state; const subdomain = getSubdomain(); diff --git a/js/components/whitelabel/prize/simple_prize/prize_app.js b/js/components/whitelabel/prize/simple_prize/prize_app.js index da02a597..d2a440dc 100644 --- a/js/components/whitelabel/prize/simple_prize/prize_app.js +++ b/js/components/whitelabel/prize/simple_prize/prize_app.js @@ -13,17 +13,14 @@ import { getSubdomain } from '../../../../utils/general_utils'; let PrizeApp = React.createClass({ propTypes: { + activeRoute: React.PropTypes.object.isRequired, + children: React.PropTypes.element.isRequired, history: React.PropTypes.object.isRequired, - routes: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, - - children: React.PropTypes.oneOfType([ - React.PropTypes.arrayOf(React.PropTypes.element), - React.PropTypes.element - ]) + routes: React.PropTypes.arrayOf(React.PropTypes.object).isRequired }, render() { - const { children, history, routes } = this.props; + const { activeRoute, children, history, routes } = this.props; const subdomain = getSubdomain(); // The second element of routes is always the active component object, where we can diff --git a/js/components/whitelabel/wallet/wallet_app.js b/js/components/whitelabel/wallet/wallet_app.js index 3940d89e..7f30e203 100644 --- a/js/components/whitelabel/wallet/wallet_app.js +++ b/js/components/whitelabel/wallet/wallet_app.js @@ -11,17 +11,14 @@ import { getSubdomain } from '../../../utils/general_utils'; let WalletApp = React.createClass({ propTypes: { + activeRoute: React.PropTypes.object.isRequired, + children: React.PropTypes.element.isRequired, history: React.PropTypes.object.isRequired, - routes: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, - - children: React.PropTypes.oneOfType([ - React.PropTypes.arrayOf(React.PropTypes.element), - React.PropTypes.element - ]) + routes: React.PropTypes.arrayOf(React.PropTypes.object).isRequired }, render() { - const { children, history, routes } = this.props; + const { activeRoute, children, history, routes } = this.props; const subdomain = getSubdomain(); // The second element of routes is always the active component object, where we can diff --git a/js/routes.js b/js/routes.js index 24df99c0..d7a1d0c4 100644 --- a/js/routes.js +++ b/js/routes.js @@ -6,7 +6,7 @@ import { Route } from 'react-router'; import getPrizeRoutes from './components/whitelabel/prize/prize_routes'; import getWalletRoutes from './components/whitelabel/wallet/wallet_routes'; -import App from './components/ascribe_app'; +import AscribeApp from './components/ascribe_app'; import PieceList from './components/piece_list'; import PieceContainer from './components/ascribe_detail/piece_container'; @@ -29,14 +29,14 @@ import { ProxyHandler, AuthRedirect } from './components/ascribe_routes/proxy_ha const COMMON_ROUTES = ( - + + headerTitle='+ NEW WORK' /> + component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(SettingsContainer)} /> + component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(ContractSettings)} /> From e616576fa538ace8bf56f0eb6181ea7a054cc1d7 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Mon, 1 Feb 2016 14:49:48 +0100 Subject: [PATCH 79/94] Hide the footer if a route has the `hideFooter` attribute --- js/components/app_base.js | 2 -- js/components/ascribe_app.js | 2 ++ js/components/footer.js | 8 ++++++-- js/components/whitelabel/prize/portfolioreview/pr_app.js | 2 ++ js/components/whitelabel/prize/simple_prize/prize_app.js | 2 ++ js/components/whitelabel/wallet/wallet_app.js | 2 ++ 6 files changed, 14 insertions(+), 4 deletions(-) diff --git a/js/components/app_base.js b/js/components/app_base.js index 4b5d460e..de1aab71 100644 --- a/js/components/app_base.js +++ b/js/components/app_base.js @@ -4,7 +4,6 @@ import React from 'react'; import classNames from 'classnames'; import { History } from 'react-router'; -import Footer from './footer'; import GlobalNotification from './global_notification'; import AppConstants from '../constants/application_constants'; @@ -48,7 +47,6 @@ export default function AppBase(App) { return (
    -
    diff --git a/js/components/ascribe_app.js b/js/components/ascribe_app.js index 0e28aa97..737a35f5 100644 --- a/js/components/ascribe_app.js +++ b/js/components/ascribe_app.js @@ -3,6 +3,7 @@ import React from 'react'; import AppBase from './app_base'; +import Footer from './footer'; import Header from './header'; @@ -23,6 +24,7 @@ let AscribeApp = React.createClass({ {/* Routes are injected here */} {children}
    +
    ); } diff --git a/js/components/footer.js b/js/components/footer.js index f2e35dfc..3010da4d 100644 --- a/js/components/footer.js +++ b/js/components/footer.js @@ -5,8 +5,12 @@ import React from 'react'; import { getLangText } from '../utils/lang_utils'; let Footer = React.createClass({ + propTypes: { + activeRoute: React.PropTypes.object.isRequired + }, + render() { - return ( + return !this.props.activeRoute.hideFooter ? (


    @@ -24,7 +28,7 @@ let Footer = React.createClass({

    - ); + ) : null; } }); diff --git a/js/components/whitelabel/prize/portfolioreview/pr_app.js b/js/components/whitelabel/prize/portfolioreview/pr_app.js index 04b985db..c738c0bd 100644 --- a/js/components/whitelabel/prize/portfolioreview/pr_app.js +++ b/js/components/whitelabel/prize/portfolioreview/pr_app.js @@ -11,6 +11,7 @@ import UserActions from '../../../../actions/user_actions'; import Hero from './components/pr_hero'; import AppBase from '../../../app_base'; +import Footer from '../../../footer'; import Header from '../../../header'; import { getSubdomain } from '../../../../utils/general_utils'; @@ -85,6 +86,7 @@ let PRApp = React.createClass({ {/* Routes are injected here */} {children}
    +
    ); } diff --git a/js/components/whitelabel/prize/simple_prize/prize_app.js b/js/components/whitelabel/prize/simple_prize/prize_app.js index d2a440dc..e3f1e290 100644 --- a/js/components/whitelabel/prize/simple_prize/prize_app.js +++ b/js/components/whitelabel/prize/simple_prize/prize_app.js @@ -6,6 +6,7 @@ import classNames from 'classnames'; import Hero from './components/prize_hero'; import AppBase from '../../../app_base'; +import Footer from '../../../footer'; import Header from '../../../header'; import { getSubdomain } from '../../../../utils/general_utils'; @@ -42,6 +43,7 @@ let PrizeApp = React.createClass({ {/* Routes are injected here */} {children}
    +
    ); } diff --git a/js/components/whitelabel/wallet/wallet_app.js b/js/components/whitelabel/wallet/wallet_app.js index 7f30e203..c7afaa01 100644 --- a/js/components/whitelabel/wallet/wallet_app.js +++ b/js/components/whitelabel/wallet/wallet_app.js @@ -4,6 +4,7 @@ import React from 'react'; import classNames from 'classnames'; import AppBase from '../../app_base'; +import Footer from '../../footer'; import Header from '../../header'; import { getSubdomain } from '../../../utils/general_utils'; @@ -43,6 +44,7 @@ let WalletApp = React.createClass({ {/* Routes are injected here */} {children}
    +
    ); } From 6f2424d1035f4a3a50f32cce8ba17ec6b71bdd01 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Thu, 4 Feb 2016 13:32:00 +0100 Subject: [PATCH 80/94] Rebase remerge --- .../whitelabel/prize/prize_routes.js | 90 +++++-- js/components/whitelabel/wallet/wallet_app.js | 5 +- .../whitelabel/wallet/wallet_routes.js | 241 +++++++++++++----- .../portfolioreview_custom_style.scss | 1 - .../simple_prize_custom_style.scss | 2 - .../wallet/23vivi/23vivi_custom_style.scss | 5 +- .../whitelabel/wallet/cc/cc_custom_style.scss | 11 +- .../wallet/cyland/cyland_custom_style.scss | 5 +- .../wallet/ikonotv/ikonotv_custom_style.scss | 44 ++-- sass/whitelabel/wallet/index.scss | 1 - 10 files changed, 269 insertions(+), 136 deletions(-) diff --git a/js/components/whitelabel/prize/prize_routes.js b/js/components/whitelabel/prize/prize_routes.js index 5f80b30c..73608ae2 100644 --- a/js/components/whitelabel/prize/prize_routes.js +++ b/js/components/whitelabel/prize/prize_routes.js @@ -31,74 +31,116 @@ import { AuthPrizeRoleRedirect } from './portfolioreview/components/pr_routes/pr const ROUTES = { sluice: ( - + + component={ProxyHandler(AuthRedirect({to: '/collection', when: 'loggedIn'}))(SPLoginContainer)} + hideFooter /> + component={ProxyHandler(AuthRedirect({to: '/', when: 'loggedOut'}))(LogoutContainer)} + hideFooter /> + component={ProxyHandler(AuthRedirect({to: '/collection', when: 'loggedIn'}))(SPSignupContainer)} + hideFooter /> + component={ProxyHandler(AuthRedirect({to: '/collection', when: 'loggedIn'}))(PasswordResetContainer)} + hideFooter /> + component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(SPSettingsContainer)} + hideFooter /> + headerTitle='+ NEW WORK' + hideFooter /> - - - - + headerTitle='COLLECTION' + hideFooter /> + + + + ), portfolioreview: ( - + + component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(PRRegisterPiece)} + hideFooter /> + headerTitle='SUBMISSIONS' + hideFooter /> + )(SPLoginContainer)} + hideFooter /> + component={ProxyHandler(AuthRedirect({to: '/', when: 'loggedOut'}))(LogoutContainer)} + hideFooter /> + )(SPSignupContainer)} + hideFooter /> + )(PasswordResetContainer)} + hideFooter /> - - - - + component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(SPSettingsContainer)} + hideFooter /> + + + + ) }; diff --git a/js/components/whitelabel/wallet/wallet_app.js b/js/components/whitelabel/wallet/wallet_app.js index c7afaa01..10ebd2b3 100644 --- a/js/components/whitelabel/wallet/wallet_app.js +++ b/js/components/whitelabel/wallet/wallet_app.js @@ -28,8 +28,9 @@ let WalletApp = React.createClass({ let header = null; // if the path of the current activeRoute is not defined, then this is the IndexRoute - if ((!path || history.isActive('/login') || history.isActive('/signup') || history.isActive('/contract_notifications')) - && (['cyland', 'ikonotv', 'lumenus', '23vivi']).indexOf(subdomain) > -1) { + if ((!path || history.isActive('/login') || history.isActive('/signup') || + history.isActive('/contract_notifications')) && + (['cyland', 'ikonotv', 'lumenus', '23vivi']).includes(subdomain)) { header = (
    ); } else { header = (
    ); diff --git a/js/components/whitelabel/wallet/wallet_routes.js b/js/components/whitelabel/wallet/wallet_routes.js index f1613ee5..bf2008c3 100644 --- a/js/components/whitelabel/wallet/wallet_routes.js +++ b/js/components/whitelabel/wallet/wallet_routes.js @@ -48,193 +48,302 @@ import WalletApp from './wallet_app'; let ROUTES = { 'cyland': ( - + + component={ProxyHandler(AuthRedirect({to: '/collection', when: 'loggedIn'}))(LoginContainer)} + hideFooter /> + component={ProxyHandler(AuthRedirect({to: '/', when: 'loggedOut'}))(LogoutContainer)} + hideFooter /> + component={ProxyHandler(AuthRedirect({to: '/collection', when: 'loggedIn'}))(SignupContainer)} + hideFooter /> + component={ProxyHandler(AuthRedirect({to: '/collection', when: 'loggedIn'}))(PasswordResetContainer)} + hideFooter /> + component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(SettingsContainer)} + hideFooter /> + component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(ContractSettings)} + hideFooter /> + aclName='acl_wallet_submit' + hideFooter /> - - - - + disableOn='noPieces' + hideFooter /> + + + + ), 'cc': ( + component={ProxyHandler(AuthRedirect({to: '/collection', when: 'loggedIn'}))(LoginContainer)} + hideFooter /> + component={ProxyHandler(AuthRedirect({to: '/', when: 'loggedOut'}))(LogoutContainer)} + hideFooter /> + component={ProxyHandler(AuthRedirect({to: '/collection', when: 'loggedIn'}))(SignupContainer)} + hideFooter /> + component={ProxyHandler(AuthRedirect({to: '/collection', when: 'loggedIn'}))(PasswordResetContainer)} + hideFooter /> + component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(SettingsContainer)} + hideFooter /> + component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(ContractSettings)} + hideFooter /> + headerTitle='+ NEW WORK' + hideFooter /> - - - - + disableOn='noPieces' + hideFooter /> + + + + ), 'ikonotv': ( - + + component={ProxyHandler(AuthRedirect({to: '/collection', when: 'loggedIn'}))(LoginContainer)} + hideFooter /> + component={ProxyHandler(AuthRedirect({to: '/', when: 'loggedOut'}))(LogoutContainer)} + hideFooter /> + component={ProxyHandler(AuthRedirect({to: '/collection', when: 'loggedIn'}))(SignupContainer)} + hideFooter /> + component={ProxyHandler(AuthRedirect({to: '/collection', when: 'loggedIn'}))(PasswordResetContainer)} + hideFooter /> + component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(SettingsContainer)} + hideFooter /> + component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(ContractSettings)} + hideFooter /> + aclName='acl_create_contractagreement' + hideFooter /> + aclName='acl_wallet_submit' + hideFooter /> + disableOn='noPieces' + hideFooter /> - - - - + component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(IkonotvContractNotifications)} + hideFooter /> + + + + ), 'lumenus': ( - + + component={ProxyHandler(AuthRedirect({to: '/collection', when: 'loggedIn'}))(LoginContainer)} + hideFooter /> + component={ProxyHandler(AuthRedirect({to: '/', when: 'loggedOut'}))(LogoutContainer)} + hideFooter /> + component={ProxyHandler(AuthRedirect({to: '/collection', when: 'loggedIn'}))(SignupContainer)} + hideFooter /> + component={ProxyHandler(AuthRedirect({to: '/collection', when: 'loggedIn'}))(PasswordResetContainer)} + hideFooter /> + component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(SettingsContainer)} + hideFooter /> + component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(ContractSettings)} + hideFooter /> + aclName='acl_wallet_submit' + hideFooter /> - - - - + disableOn='noPieces' + hideFooter /> + + + + ), '23vivi': ( - + + component={ProxyHandler(AuthRedirect({to: '/collection', when: 'loggedIn'}))(LoginContainer)} + hideFooter /> + component={ProxyHandler(AuthRedirect({to: '/', when: 'loggedOut'}))(LogoutContainer)} + hideFooter /> + component={ProxyHandler(AuthRedirect({to: '/collection', when: 'loggedIn'}))(SignupContainer)} + hideFooter /> + component={ProxyHandler(AuthRedirect({to: '/collection', when: 'loggedIn'}))(PasswordResetContainer)} + hideFooter /> + component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(SettingsContainer)} + hideFooter /> + component={ProxyHandler(AuthRedirect({to: '/login', when: 'loggedOut'}))(ContractSettings)} + hideFooter /> + aclName='acl_wallet_submit' + hideFooter /> - - - - + disableOn='noPieces' + hideFooter /> + + + + ) }; diff --git a/sass/whitelabel/prize/portfolioreview/portfolioreview_custom_style.scss b/sass/whitelabel/prize/portfolioreview/portfolioreview_custom_style.scss index b0027bff..385f4ed7 100644 --- a/sass/whitelabel/prize/portfolioreview/portfolioreview_custom_style.scss +++ b/sass/whitelabel/prize/portfolioreview/portfolioreview_custom_style.scss @@ -2,7 +2,6 @@ $pr--nav-fg-prim-color: black; $pr--button-color: $pr--nav-fg-prim-color; .client--portfolioreview { - .btn-wide, .btn-default { background-color: $pr--button-color; diff --git a/sass/whitelabel/prize/simple_prize/simple_prize_custom_style.scss b/sass/whitelabel/prize/simple_prize/simple_prize_custom_style.scss index 8c46d366..e022f34e 100644 --- a/sass/whitelabel/prize/simple_prize/simple_prize_custom_style.scss +++ b/sass/whitelabel/prize/simple_prize/simple_prize_custom_style.scss @@ -68,5 +68,3 @@ padding: 0.7em; } - - diff --git a/sass/whitelabel/wallet/23vivi/23vivi_custom_style.scss b/sass/whitelabel/wallet/23vivi/23vivi_custom_style.scss index 3e6e20cf..f09d9b13 100644 --- a/sass/whitelabel/wallet/23vivi/23vivi_custom_style.scss +++ b/sass/whitelabel/wallet/23vivi/23vivi_custom_style.scss @@ -13,6 +13,7 @@ $vivi23--highlight-color: #de2600; /** Landing page **/ .route--landing { display: table; + min-height: 100vh; > .container { display: table-cell; @@ -252,10 +253,6 @@ $vivi23--highlight-color: #de2600; display: none; } - .ascribe-footer { - display: none; - } - .ascribe-accordion-list-table-toggle:hover { color: $vivi23--fg-color; } diff --git a/sass/whitelabel/wallet/cc/cc_custom_style.scss b/sass/whitelabel/wallet/cc/cc_custom_style.scss index 774f5b27..348fca1f 100644 --- a/sass/whitelabel/wallet/cc/cc_custom_style.scss +++ b/sass/whitelabel/wallet/cc/cc_custom_style.scss @@ -55,16 +55,11 @@ $cc--button-color: $cc--nav-fg-prim-color; } } -.client--cc .ascribe-footer { - display: none; -} - - -.client--cc .icon-ascribe-search{ +.client--cc .icon-ascribe-search { color: $cc--button-color; } -.client--cc .ascribe-piece-list-toolbar .btn-ascribe-add{ +.client--cc .ascribe-piece-list-toolbar .btn-ascribe-add { display: none; } @@ -223,4 +218,4 @@ $cc--button-color: $cc--nav-fg-prim-color; .client--cc .upload-button-wrapper > span { color: $cc--button-color; -} \ No newline at end of file +} diff --git a/sass/whitelabel/wallet/cyland/cyland_custom_style.scss b/sass/whitelabel/wallet/cyland/cyland_custom_style.scss index 549b2004..0a7226ac 100644 --- a/sass/whitelabel/wallet/cyland/cyland_custom_style.scss +++ b/sass/whitelabel/wallet/cyland/cyland_custom_style.scss @@ -56,10 +56,6 @@ $cyland--button-sec-color: #515151; } } -.client--cyland .ascribe-footer { - display: none; -} - .client--cyland .icon-ascribe-search { color: $cyland--button-color; } @@ -171,6 +167,7 @@ $cyland--button-sec-color: #515151; .client--cyland { .route--landing { display: table; + min-height: 100vh; > .container { display: table-cell; diff --git a/sass/whitelabel/wallet/ikonotv/ikonotv_custom_style.scss b/sass/whitelabel/wallet/ikonotv/ikonotv_custom_style.scss index 8f330911..1d7a4461 100644 --- a/sass/whitelabel/wallet/ikonotv/ikonotv_custom_style.scss +++ b/sass/whitelabel/wallet/ikonotv/ikonotv_custom_style.scss @@ -108,24 +108,36 @@ $ikono--font: 'Helvetica Neue', 'Helvetica', sans-serif !important; } +.client--ikonotv { + .route--landing, + .route--login, + .route--signup { + background-color: $ikono--landing-bg-color; + min-height: 100vh; + } + + .route--login, + .route--signup { + .ascribe-form-bordered { + border: none; + } + } +} + .client--ikonotv .route--landing { - background-color: $ikono--landing-bg-color; animation: color-loop 20s; -o-animation: color-loop 20s infinite; -ms-animation: color-loop 20s infinite; -moz-animation: color-loop 20s infinite; -webkit-animation: color-loop 20s infinite; - + margin: 0; width: 100%; padding: 5em 1em; } - .client--ikonotv .route--login, .client--ikonotv .route--signup { - background-color: $ikono--landing-bg-color; - .btn-wide { display: block; margin: 50px auto 0; @@ -209,27 +221,11 @@ $ikono--font: 'Helvetica Neue', 'Helvetica', sans-serif !important; } -.client--ikonotv { - .route--login, - .route--signup { - .ascribe-form-bordered { - border: none; - } - } -} - -.client--ikonotv .ascribe-login-wrapper { -} - -.client--ikonotv .ascribe-footer { - display: none; -} - -.client--ikonotv .icon-ascribe-search{ +.client--ikonotv .icon-ascribe-search { color: $ikono--button-color; } -.client--ikonotv .ascribe-piece-list-toolbar .btn-ascribe-add{ +.client--ikonotv .ascribe-piece-list-toolbar .btn-ascribe-add { display: none; } @@ -560,4 +556,4 @@ $ikono--font: 'Helvetica Neue', 'Helvetica', sans-serif !important; .client--ikonotv .upload-button-wrapper > span { color: $ikono--button-color; -} \ No newline at end of file +} diff --git a/sass/whitelabel/wallet/index.scss b/sass/whitelabel/wallet/index.scss index 647bb16c..f480ac8d 100644 --- a/sass/whitelabel/wallet/index.scss +++ b/sass/whitelabel/wallet/index.scss @@ -5,7 +5,6 @@ .ascribe-wallet-app { border-radius: 0; - min-height: 100vh; padding-top: 70px; padding-bottom: 10px; } From 8f2ace77af8da1ceecf80ce664d3572fcdb1857b Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Wed, 3 Feb 2016 12:28:49 +0100 Subject: [PATCH 81/94] Use previous method from white labels to get the current active route --- js/components/app_base.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/js/components/app_base.js b/js/components/app_base.js index de1aab71..c188e628 100644 --- a/js/components/app_base.js +++ b/js/components/app_base.js @@ -37,13 +37,11 @@ export default function AppBase(App) { }, render() { - const { children } = this.props; + const { routes } = this.props; - // Get the currently active route of the app by using the injected route parameter - // on the currently active child route. - // Note that despite its name, this.props.children can only ever be a single - // React.PropTypes.element. - const activeRoute = children.props.route; + // The second element of the routes prop given to us by react-router is always the + // active second-level component object (ie. after App). + const activeRoute = routes[1]; return (
    From f1b677dbd16a5dc85331b148a2e4f5980048ab67 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Wed, 3 Feb 2016 12:38:34 +0100 Subject: [PATCH 82/94] Use the activeRoute prop to generate route classnames on whitelabels --- js/components/whitelabel/prize/portfolioreview/pr_app.js | 5 +---- js/components/whitelabel/prize/simple_prize/prize_app.js | 5 +---- js/components/whitelabel/wallet/wallet_app.js | 5 +---- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/js/components/whitelabel/prize/portfolioreview/pr_app.js b/js/components/whitelabel/prize/portfolioreview/pr_app.js index c738c0bd..73df2766 100644 --- a/js/components/whitelabel/prize/portfolioreview/pr_app.js +++ b/js/components/whitelabel/prize/portfolioreview/pr_app.js @@ -61,10 +61,7 @@ let PRApp = React.createClass({ const { activeRoute, children, history, routes } = this.props; const { currentUser } = this.state; const subdomain = getSubdomain(); - - // The second element of routes is always the active component object, where we can - // extract the path. - const path = routes[1] ? routes[1].path : null; + const path = activeRoute && activeRoute.path; let style = {}; let header; diff --git a/js/components/whitelabel/prize/simple_prize/prize_app.js b/js/components/whitelabel/prize/simple_prize/prize_app.js index e3f1e290..97ccb53c 100644 --- a/js/components/whitelabel/prize/simple_prize/prize_app.js +++ b/js/components/whitelabel/prize/simple_prize/prize_app.js @@ -23,10 +23,7 @@ let PrizeApp = React.createClass({ render() { const { activeRoute, children, history, routes } = this.props; const subdomain = getSubdomain(); - - // The second element of routes is always the active component object, where we can - // extract the path. - const path = routes[1] ? routes[1].path : null; + const path = activeRoute && activeRoute.path; let header = null; // if the path of the current activeRoute is not defined, then this is the IndexRoute diff --git a/js/components/whitelabel/wallet/wallet_app.js b/js/components/whitelabel/wallet/wallet_app.js index 10ebd2b3..ec709ffc 100644 --- a/js/components/whitelabel/wallet/wallet_app.js +++ b/js/components/whitelabel/wallet/wallet_app.js @@ -21,10 +21,7 @@ let WalletApp = React.createClass({ render() { const { activeRoute, children, history, routes } = this.props; const subdomain = getSubdomain(); - - // The second element of routes is always the active component object, where we can - // extract the path. - const path = routes[1] ? routes[1].path : null; + const path = activeRoute && activeRoute.path; let header = null; // if the path of the current activeRoute is not defined, then this is the IndexRoute From 3bb1f65f38696562dbdff5d9aeb7a8c02f8358cb Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Thu, 4 Feb 2016 13:40:40 +0100 Subject: [PATCH 83/94] Add whitespace for different npm test scripts --- package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package.json b/package.json index 5e72b863..a40c19bb 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,10 @@ "build": "gulp build --production", "start": "node server.js", "test": "npm run sauce-test", + "sauce-test": "mocha ./test/integration/tests/", "tunnel": "node ./test/integration/tunnel.js", + "vi-clean": "rm -rf ./gemini-report", "vi-phantom": "phantomjs --webdriver=4444", "vi-update": "gemini update -c ./test/gemini/.gemini.yml", From 9c073bb73bbb6a25477c0dacc168ad1ce395b09b Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Thu, 4 Feb 2016 15:02:50 +0100 Subject: [PATCH 84/94] Fix typo in gemini test output for environment configuration --- test/gemini/tests/environment.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/gemini/tests/environment.js b/test/gemini/tests/environment.js index 32f1c8ee..abc5e436 100644 --- a/test/gemini/tests/environment.js +++ b/test/gemini/tests/environment.js @@ -22,9 +22,9 @@ console.log(`Main piece: ${MAIN_PIECE_ID}`); console.log(`Main edition: ${MAIN_EDITION_ID}\n`); console.log('Timeouts:'); console.log(` Short: ${TIMEOUTS.SHORT}`); -console.log(` Normal: ${TIMEOUTS.NORMAL}\n`); -console.log(` Long: ${TIMEOUTS.LONG}\n`); -console.log(` Super super extra long: ${TIMEOUTS.SUPER_DUPER_EXTRA_LONG}\n`); +console.log(` Normal: ${TIMEOUTS.NORMAL}`); +console.log(` Long: ${TIMEOUTS.LONG}`); +console.log(` Super duper extra long: ${TIMEOUTS.SUPER_DUPER_EXTRA_LONG}\n`); console.log('========================================================\n'); module.exports = { From 826ca080731c9fcbd3f9d72e61c585b65f661548 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Fri, 5 Feb 2016 10:38:59 +0100 Subject: [PATCH 85/94] Merge with master --- .env-template | 3 + .eslintrc | 2 +- .gitignore | 11 +- README.md | 91 +++++-- gulpfile.js | 3 +- js/actions/contract_list_actions.js | 16 +- js/actions/edition_actions.js | 6 +- js/actions/edition_list_actions.js | 24 +- js/actions/facebook_actions.js | 14 + js/actions/piece_actions.js | 19 +- js/actions/piece_list_actions.js | 4 +- js/actions/prize_list_actions.js | 34 --- js/app.js | 86 +++--- js/components/app_base.js | 90 ++++++ js/components/app_route_wrapper.js | 2 +- .../ascribe_accordion_list/accordion_list.js | 7 +- .../accordion_list_item_edition_widget.js | 12 +- .../accordion_list_item_table_editions.js | 20 +- ...cordion_list_item_thumbnail_placeholder.js | 2 +- .../accordion_list_item_wallet.js | 19 +- js/components/ascribe_app.js | 56 +--- .../ascribe_buttons/acls/acl_button.js | 2 +- .../ascribe_buttons/create_editions_button.js | 34 +-- js/components/ascribe_detail/edition.js | 2 +- .../ascribe_detail/edition_action_panel.js | 7 +- .../ascribe_detail/edition_container.js | 27 +- .../ascribe_detail/further_details.js | 55 ++-- .../further_details_fileuploader.js | 22 +- .../ascribe_detail/media_container.js | 20 +- js/components/ascribe_detail/piece.js | 2 +- .../ascribe_detail/piece_container.js | 79 +++--- .../ascribe_forms/create_editions_form.js | 7 +- js/components/ascribe_forms/form.js | 15 +- js/components/ascribe_forms/form_consign.js | 3 +- .../form_copyright_association.js | 5 +- .../ascribe_forms/form_create_contract.js | 19 +- js/components/ascribe_forms/form_loan.js | 5 +- .../ascribe_forms/form_piece_extradata.js | 39 +-- .../ascribe_forms/form_register_piece.js | 20 +- .../form_send_contract_agreement.js | 3 +- js/components/ascribe_forms/form_signup.js | 3 +- .../ascribe_forms/form_submit_to_prize.js | 3 +- .../input_contract_agreement_checkbox.js | 2 +- js/components/ascribe_forms/input_date.js | 31 ++- .../ascribe_forms/input_fineuploader.js | 6 +- .../ascribe_forms/input_textarea_toggable.js | 41 +-- js/components/ascribe_forms/property.js | 12 +- js/components/ascribe_modal/modal_wrapper.js | 41 ++- .../piece_list_toolbar_filter_widget.js | 1 + .../piece_list_toolbar_order_widget.js | 7 +- .../ascribe_settings/account_settings.js | 5 +- .../ascribe_settings/contract_settings.js | 32 +-- .../contract_settings_update_button.js | 115 ++++---- .../ascribe_settings/webhook_settings.js | 37 +-- .../facebook_share_button.js | 50 ++-- .../file_drag_and_drop_preview.js | 17 +- .../file_drag_and_drop_preview_other.js | 19 +- .../ascribe_upload_button/upload_button.js | 47 ++-- .../react_s3_fine_uploader.js | 224 +++++++-------- js/components/coa_verify_container.js | 82 +++--- js/components/error_not_found_page.js | 14 + js/components/footer.js | 8 +- js/components/global_action.js | 43 --- js/components/header.js | 33 ++- js/components/header_notification.js | 1 + js/components/nav_routes_links.js | 73 +++-- js/components/nav_routes_links_link.js | 32 ++- js/components/piece_list.js | 46 +++- js/components/register_piece.js | 16 +- .../pr_forms/pr_register_piece_form.js | 46 ++-- .../prize/portfolioreview/pr_app.js | 78 ++---- .../whitelabel/prize/prize_routes.js | 90 ++++-- .../accordion_list_item_prize.js | 10 +- .../ascribe_detail/prize_piece_container.js | 167 ++++++------ .../components/prize_piece_list.js | 7 +- .../components/prize_register_piece.js | 3 +- .../prize/simple_prize/prize_app.js | 62 +---- .../simple_prize/stores/prize_rating_store.js | 26 +- .../sluice_selected_prize_action_button.js | 2 +- ...cordion_list_item_thumbnail_placeholder.js | 2 +- .../components/23vivi/23vivi_landing.js | 2 +- .../components/23vivi/23vivi_piece_list.js | 2 +- .../cyland_accordion_list_item.js | 5 +- .../cyland_detail/cyland_piece_container.js | 35 ++- .../cyland_additional_data_form.js | 71 ++--- .../components/cyland/cyland_landing.js | 2 +- .../components/cyland/cyland_piece_list.js | 40 ++- .../cyland/cyland_register_piece.js | 49 ++-- .../ikonotv_accordion_list_item.js | 5 +- .../ikonotv/ikonotv_contract_notifications.js | 2 +- .../ikonotv_detail/ikonotv_piece_container.js | 34 +-- .../ikonotv_artist_details_form.js | 56 ++-- .../ikonotv_artwork_details_form.js | 64 +++-- .../components/ikonotv/ikonotv_piece_list.js | 22 +- .../ikonotv/ikonotv_register_piece.js | 63 ++--- .../market_buttons/market_submit_button.js | 54 ++-- .../market_detail/market_further_details.js | 6 +- .../market_additional_data_form.js | 157 ++++++----- .../components/market/market_piece_list.js | 11 +- .../market/market_register_piece.js | 67 +++-- js/components/whitelabel/wallet/wallet_app.js | 84 ++---- .../whitelabel/wallet/wallet_routes.js | 246 ++++++++++++----- js/constants/api_urls.js | 3 +- js/constants/application_constants.js | 33 +-- js/constants/uploader_constants.js | 32 +++ js/fetchers/edition_list_fetcher.js | 6 +- js/fetchers/ownership_fetcher.js | 14 +- js/fetchers/piece_fetcher.js | 15 - js/fetchers/piece_list_fetcher.js | 6 +- js/fetchers/prize_list_fetcher.js | 12 - js/history.js | 8 +- js/routes.js | 25 +- js/sources/NAMING_CONVENTIONS.md | 10 +- js/sources/coa_source.js | 11 +- js/sources/edition_source.js | 6 +- js/sources/piece_source.js | 19 ++ js/sources/user_source.js | 11 +- js/sources/webhook_source.js | 25 +- js/sources/whitelabel_source.js | 13 +- js/stores/edition_list_store.js | 123 +++++---- js/stores/edition_store.js | 62 +++-- js/stores/piece_list_store.js | 17 +- js/stores/piece_store.js | 59 +++- js/stores/prize_list_store.js | 20 -- js/stores/user_store.js | 16 +- js/stores/webhook_store.js | 53 ++-- js/stores/whitelabel_store.js | 14 +- js/third_party/{debug.js => debug_handler.js} | 1 - .../{facebook.js => facebook_handler.js} | 17 +- js/third_party/{ga.js => ga_handler.js} | 0 .../{intercom.js => intercom_handler.js} | 0 ...ifications.js => notifications_handler.js} | 0 js/third_party/{raven.js => raven_handler.js} | 0 js/utils/constants_utils.js | 14 +- js/utils/dom_utils.js | 82 +++++- js/utils/error_utils.js | 13 +- js/utils/file_utils.js | 4 +- js/utils/regex_utils.js | 55 +++- js/utils/url_utils.js | 2 +- package.json | 36 ++- phantomjs/launch_app_and_login.js | 62 +++++ sass/ascribe_custom_style.scss | 4 +- sass/ascribe_global_notification.scss | 60 ++-- sass/ascribe_property.scss | 8 +- sass/ascribe_social_share.scss | 4 + sass/ascribe_textarea.scss | 2 +- sass/ascribe_uploader.scss | 47 ++-- sass/ascribe_variables.scss | 1 + sass/whitelabel/prize/index.scss | 3 +- .../portfolioreview_custom_style.scss | 3 +- .../simple_prize_custom_style.scss | 191 +------------ .../simple_prize/simple_prize_variables.scss | 3 + .../prize/sluice/sluice_custom_style.scss | 182 +++++++++++++ .../wallet/23vivi/23vivi_custom_style.scss | 12 +- .../whitelabel/wallet/cc/cc_custom_style.scss | 11 +- .../wallet/cyland/cyland_custom_style.scss | 10 +- .../wallet/ikonotv/ikonotv_custom_style.scss | 44 ++- sass/whitelabel/wallet/index.scss | 1 - test/.eslintrc | 36 +++ test/gemini/.gemini.yml | 133 +++++++++ test/gemini/README.md | 208 ++++++++++++++ test/gemini/tests/environment.js | 35 +++ test/gemini/tests/main/authenticated.js | 218 +++++++++++++++ test/gemini/tests/main/basic.js | 148 ++++++++++ test/gemini/tests/main/detail.js | 134 +++++++++ test/gemini/tests/whitelabel/23vivi/23vivi.js | 29 ++ test/gemini/tests/whitelabel/cyland/cyland.js | 30 ++ .../tests/whitelabel/ikonotv/ikonotv.js | 98 +++++++ .../tests/whitelabel/lumenus/lumenus.js | 29 ++ .../whitelabel/shared/whitelabel_basic.js | 115 ++++++++ test/integration/README.md | 256 ++++++++++++++++++ test/integration/config.js | 18 ++ test/integration/setup.js | 50 ++++ test/integration/tests/test-login.js | 50 ++++ test/integration/tunnel.js | 23 ++ 175 files changed, 4418 insertions(+), 2175 deletions(-) create mode 100644 .env-template create mode 100644 js/actions/facebook_actions.js delete mode 100644 js/actions/prize_list_actions.js create mode 100644 js/components/app_base.js delete mode 100644 js/components/global_action.js create mode 100644 js/constants/uploader_constants.js delete mode 100644 js/fetchers/piece_fetcher.js delete mode 100644 js/fetchers/prize_list_fetcher.js create mode 100644 js/sources/piece_source.js delete mode 100644 js/stores/prize_list_store.js rename js/third_party/{debug.js => debug_handler.js} (99%) rename js/third_party/{facebook.js => facebook_handler.js} (67%) rename js/third_party/{ga.js => ga_handler.js} (100%) rename js/third_party/{intercom.js => intercom_handler.js} (100%) rename js/third_party/{notifications.js => notifications_handler.js} (100%) rename js/third_party/{raven.js => raven_handler.js} (100%) create mode 100644 phantomjs/launch_app_and_login.js create mode 100644 sass/whitelabel/prize/simple_prize/simple_prize_variables.scss create mode 100644 sass/whitelabel/prize/sluice/sluice_custom_style.scss create mode 100644 test/.eslintrc create mode 100644 test/gemini/.gemini.yml create mode 100644 test/gemini/README.md create mode 100644 test/gemini/tests/environment.js create mode 100644 test/gemini/tests/main/authenticated.js create mode 100644 test/gemini/tests/main/basic.js create mode 100644 test/gemini/tests/main/detail.js create mode 100644 test/gemini/tests/whitelabel/23vivi/23vivi.js create mode 100644 test/gemini/tests/whitelabel/cyland/cyland.js create mode 100644 test/gemini/tests/whitelabel/ikonotv/ikonotv.js create mode 100644 test/gemini/tests/whitelabel/lumenus/lumenus.js create mode 100644 test/gemini/tests/whitelabel/shared/whitelabel_basic.js create mode 100644 test/integration/README.md create mode 100644 test/integration/config.js create mode 100644 test/integration/setup.js create mode 100644 test/integration/tests/test-login.js create mode 100644 test/integration/tunnel.js diff --git a/.env-template b/.env-template new file mode 100644 index 00000000..8c4fe11c --- /dev/null +++ b/.env-template @@ -0,0 +1,3 @@ +SAUCE_USERNAME=ascribe +SAUCE_ACCESS_KEY= +SAUCE_DEFAULT_URL= diff --git a/.eslintrc b/.eslintrc index d41c0a2a..5751f3ad 100644 --- a/.eslintrc +++ b/.eslintrc @@ -2,7 +2,7 @@ "parser": "babel-eslint", "env": { "browser": true, - "es6": true + "es6": true, }, "rules": { "new-cap": [2, {newIsCap: true, capIsNew: false}], diff --git a/.gitignore b/.gitignore index 30c9eae9..e497465f 100644 --- a/.gitignore +++ b/.gitignore @@ -16,9 +16,14 @@ webapp-dependencies.txt pids logs results - + +build/* + +gemini-coverage/* +gemini-report/* +test/gemini/screenshots/* + node_modules/* -build - .DS_Store +.env diff --git a/README.md b/README.md index e07eca0d..8dcf78ad 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,19 @@ Introduction ============ -Onion is the web client for Ascribe. The idea is to have a well documented, -easy to test, easy to hack, JavaScript application. +Onion is the web client for Ascribe. The idea is to have a well documented, modern, easy to test, easy to hack, JavaScript application. -The code is JavaScript ECMA 6. +The code is JavaScript 2015 / ECMAScript 6. Getting started =============== + Install some nice extension for Chrom(e|ium): - [React Developer Tools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi) - +- [Alt Developer Tools](https://github.com/goatslacker/alt-devtool) + ```bash git clone git@github.com:ascribe/onion.git cd onion @@ -37,43 +38,62 @@ Additionally, to work on the white labeling functionality, you need to edit your JavaScript Code Conventions =========================== + For this project, we're using: * 4 Spaces -* We use ES6 +* ES6 * We don't use ES6's class declaration for React components because it does not support Mixins as well as Autobinding ([Blog post about it](http://facebook.github.io/react/blog/2015/01/27/react-v0.13.0-beta-1.html#autobinding)) * We don't use camel case for file naming but in everything Javascript related -* We use `let` instead of `var`: [SA Post](http://stackoverflow.com/questions/762011/javascript-let-keyword-vs-var-keyword) -* We don't use Javascript's `Date` object, as its interface introduced bugs previously and we're including `momentjs` for other dependencies anyways +* We use `momentjs` instead of Javascript's `Date` object, as the native `Date` interface previously introduced bugs and we're including `momentjs` for other dependencies anyway -Branch names -===================== -Since we moved to Github, we cannot create branch names automatically with JIRA anymore. -To not lose context, but still be able to switch branches quickly using a ticket's number, we're recommending the following rules when naming our branches in onion. +Make sure to check out the [style guide](https://github.com/ascribe/javascript). -``` -AD--brief-and-sane-description-of-the-ticket -``` +Linting +------- -where `brief-and-sane-description-of-the-ticket` does not need to equal to the ticket's title. -This allows JIRA to still track branches and pull-requests while allowing us to keep our peace of mind. +We use [ESLint](https://github.com/eslint/eslint) with our own [custom ruleset](.eslintrc). -Example -------------- -**JIRA ticket name:** `AD-1242 - Frontend caching for simple endpoints to measure perceived page load ` - -**Github branch name:** `AD-1242-caching-solution-for-stores` SCSS Code Conventions ===================== + Install [lint-scss](https://github.com/brigade/scss-lint), check the [editor integration docs](https://github.com/brigade/scss-lint#editor-integration) to integrate the lint in your editor. Some interesting links: * [Improving Sass code quality on theguardian.com](https://www.theguardian.com/info/developer-blog/2014/may/13/improving-sass-code-quality-on-theguardiancom) +Branch names +============ + +To allow Github and JIRA to track branches while still allowing us to switch branches quickly using a ticket's number (and keep our peace of mind), we have the following rules for naming branches: + +``` +// For issues logged in Github: +AG--brief-and-sane-description-of-the-ticket + +// For issues logged in JIRA: +AD--brief-and-sane-description-of-the-ticket +``` + +where `brief-and-sane-description-of-the-ticket` does not need to equal to the issue or ticket's title. + + +Example +------- + +**JIRA ticket name:** `AD-1242 - Frontend caching for simple endpoints to measure perceived page load ` + +**Github branch name:** `AD-1242-caching-solution-for-stores` + + Testing -=============== +======= + +Unit Testing +------------ + We're using Facebook's jest to do testing as it integrates nicely with react.js as well. Tests are always created per directory by creating a `__tests__` folder. To test a specific file, a `_tests.js` file needs to be created. @@ -83,7 +103,24 @@ This is due to the fact that jest's function mocking and ES6 module syntax are [ Therefore, to require a module in your test file, you need to use CommonJS's `require` syntax. Except for this, all tests can be written in ES6 syntax. -## Workflow +Visual Regression Testing +------------------------- + +We're using [Gemini](https://github.com/gemini-testing/gemini) for visual regression tests because it supports both PhantomJS2 and SauceLabs. + +See the [helper docs](test/gemini/README.md) for information on installing Gemini, its dependencies, and running and writing tests. + +Integration Testing +------------------- + +We're using [Sauce Labs](https://saucelabs.com/home) with [WD.js](https://github.com/admc/wd) for integration testing across browser grids with Selenium. + +See the [helper docs](test/integration/README.md) for information on each part of the test stack and how to run and write tests. + + +Workflow +======== + Generally, when you're runing `gulp serve`, all tests are being run. If you want to test exclusively (without having the obnoxious ES6Linter warnings), you can just run `gulp jest:watch`. @@ -134,9 +171,16 @@ A: Easily by starting the your gulp process with the following command: ONION_BASE_URL='/' ONION_SERVER_URL='http://localhost.com:8000/' gulp serve ``` +Or, by adding these two your environment variables: +``` +ONION_BASE_URL='/' +ONION_SERVER_URL='http://localhost.com:8000/' +``` + Q: I want to know all dependencies that get bundled into the live build. A: ```browserify -e js/app.js --list > webapp-dependencies.txt``` + Reading list ============ @@ -149,7 +193,6 @@ Start here - [alt.js](http://alt.js.org/) - [alt.js readme](https://github.com/goatslacker/alt) - Moar stuff ---------- diff --git a/gulpfile.js b/gulpfile.js index f13945b0..afa0d5a9 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -97,7 +97,8 @@ gulp.task('browser-sync', function() { proxy: 'http://localhost:4000', port: 3000, open: false, // does not open the browser-window anymore (handled manually) - ghostMode: false + ghostMode: false, + notify: false // stop showing the browsersync pop up }); }); diff --git a/js/actions/contract_list_actions.js b/js/actions/contract_list_actions.js index 1c5c0913..d368ac73 100644 --- a/js/actions/contract_list_actions.js +++ b/js/actions/contract_list_actions.js @@ -28,12 +28,10 @@ class ContractListActions { } - changeContract(contract){ + changeContract(contract) { return Q.Promise((resolve, reject) => { OwnershipFetcher.changeContract(contract) - .then((res) => { - resolve(res); - }) + .then(resolve) .catch((err)=> { console.logGlobal(err); reject(err); @@ -41,13 +39,11 @@ class ContractListActions { }); } - removeContract(contractId){ - return Q.Promise( (resolve, reject) => { + removeContract(contractId) { + return Q.Promise((resolve, reject) => { OwnershipFetcher.deleteContract(contractId) - .then((res) => { - resolve(res); - }) - .catch( (err) => { + .then(resolve) + .catch((err) => { console.logGlobal(err); reject(err); }); diff --git a/js/actions/edition_actions.js b/js/actions/edition_actions.js index 1727deff..1feee0dd 100644 --- a/js/actions/edition_actions.js +++ b/js/actions/edition_actions.js @@ -7,11 +7,11 @@ class EditionActions { constructor() { this.generateActions( 'fetchEdition', - 'successFetchEdition', 'successFetchCoa', - 'flushEdition', + 'successFetchEdition', 'errorCoa', - 'errorEdition' + 'errorEdition', + 'flushEdition' ); } } diff --git a/js/actions/edition_list_actions.js b/js/actions/edition_list_actions.js index 6f9881ee..9474a418 100644 --- a/js/actions/edition_list_actions.js +++ b/js/actions/edition_list_actions.js @@ -17,23 +17,31 @@ class EditionListActions { ); } - fetchEditionList(pieceId, page, pageSize, orderBy, orderAsc, filterBy) { - if((!orderBy && typeof orderAsc === 'undefined') || !orderAsc) { + fetchEditionList({ pieceId, page, pageSize, orderBy, orderAsc, filterBy, maxEdition }) { + if ((!orderBy && typeof orderAsc === 'undefined') || !orderAsc) { orderBy = 'edition_number'; orderAsc = true; } // Taken from: http://stackoverflow.com/a/519157/1263876 - if((typeof page === 'undefined' || !page) && (typeof pageSize === 'undefined' || !pageSize)) { + if ((typeof page === 'undefined' || !page) && (typeof pageSize === 'undefined' || !pageSize)) { page = 1; pageSize = 10; } + let itemsToFetch = pageSize; + // If we only want to fetch up to a specified edition, fetch all pages up to it + // as one page and adjust afterwards + if (typeof maxEdition === 'number') { + itemsToFetch = Math.ceil(maxEdition / pageSize) * pageSize; + page = 1; + } + return Q.Promise((resolve, reject) => { EditionListFetcher - .fetch(pieceId, page, pageSize, orderBy, orderAsc, filterBy) + .fetch({ pieceId, page, orderBy, orderAsc, filterBy, pageSize: itemsToFetch }) .then((res) => { - if(res && !res.editions) { + if (res && !res.editions) { throw new Error('Piece has no editions to fetch.'); } @@ -44,8 +52,9 @@ class EditionListActions { orderBy, orderAsc, filterBy, - 'editionListOfPiece': res.editions, - 'count': res.count + maxEdition, + count: res.count, + editionListOfPiece: res.editions }); resolve(res); }) @@ -54,7 +63,6 @@ class EditionListActions { reject(err); }); }); - } } diff --git a/js/actions/facebook_actions.js b/js/actions/facebook_actions.js new file mode 100644 index 00000000..2e784fba --- /dev/null +++ b/js/actions/facebook_actions.js @@ -0,0 +1,14 @@ +'use strict'; + +import { altThirdParty } from '../alt'; + + +class FacebookActions { + constructor() { + this.generateActions( + 'sdkReady' + ); + } +} + +export default altThirdParty.createActions(FacebookActions); diff --git a/js/actions/piece_actions.js b/js/actions/piece_actions.js index 9002e8c5..7ad3ae29 100644 --- a/js/actions/piece_actions.js +++ b/js/actions/piece_actions.js @@ -1,28 +1,19 @@ 'use strict'; import { alt } from '../alt'; -import PieceFetcher from '../fetchers/piece_fetcher'; class PieceActions { constructor() { this.generateActions( + 'fetchPiece', + 'successFetchPiece', + 'errorPiece', + 'flushPiece', 'updatePiece', - 'updateProperty', - 'pieceFailed' + 'updateProperty' ); } - - fetchOne(pieceId) { - PieceFetcher.fetchOne(pieceId) - .then((res) => { - this.actions.updatePiece(res.piece); - }) - .catch((err) => { - console.logGlobal(err); - this.actions.pieceFailed(err.json); - }); - } } export default alt.createActions(PieceActions); diff --git a/js/actions/piece_list_actions.js b/js/actions/piece_list_actions.js index 7ef9cb59..c52ef5e2 100644 --- a/js/actions/piece_list_actions.js +++ b/js/actions/piece_list_actions.js @@ -15,7 +15,7 @@ class PieceListActions { ); } - fetchPieceList(page, pageSize, search, orderBy, orderAsc, filterBy) { + fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy }) { // To prevent flickering on a pagination request, // we overwrite the piecelist with an empty list before // pieceListCount === -1 defines the loading state @@ -34,7 +34,7 @@ class PieceListActions { // afterwards, we can load the list return Q.Promise((resolve, reject) => { PieceListFetcher - .fetch(page, pageSize, search, orderBy, orderAsc, filterBy) + .fetch({ page, pageSize, search, orderBy, orderAsc, filterBy }) .then((res) => { this.actions.updatePieceList({ page, diff --git a/js/actions/prize_list_actions.js b/js/actions/prize_list_actions.js deleted file mode 100644 index da2f97df..00000000 --- a/js/actions/prize_list_actions.js +++ /dev/null @@ -1,34 +0,0 @@ -'use strict'; - -import { alt } from '../alt'; -import Q from 'q'; - -import PrizeListFetcher from '../fetchers/prize_list_fetcher'; - -class PrizeListActions { - constructor() { - this.generateActions( - 'updatePrizeList' - ); - } - - fetchPrizeList() { - return Q.Promise((resolve, reject) => { - PrizeListFetcher - .fetch() - .then((res) => { - this.actions.updatePrizeList({ - prizeList: res.prizes, - prizeListCount: res.count - }); - resolve(res); - }) - .catch((err) => { - console.logGlobal(err); - reject(err); - }); - }); - } -} - -export default alt.createActions(PrizeListActions); \ No newline at end of file diff --git a/js/app.js b/js/app.js index dc8204cf..dc8e3d62 100644 --- a/js/app.js +++ b/js/app.js @@ -1,14 +1,13 @@ 'use strict'; import 'babel/polyfill'; +import 'classlist-polyfill'; import React from 'react'; import { Router, Redirect } from 'react-router'; import history from './history'; -/* eslint-disable */ import fetch from 'isomorphic-fetch'; -/* eslint-enable */ import ApiUrls from './constants/api_urls'; @@ -17,44 +16,27 @@ import getRoutes from './routes'; import requests from './utils/requests'; import { updateApiUrls } from './constants/api_urls'; -import { getSubdomainSettings } from './utils/constants_utils'; +import { getDefaultSubdomainSettings, getSubdomainSettings } from './utils/constants_utils'; import { initLogging } from './utils/error_utils'; import { getSubdomain } from './utils/general_utils'; import EventActions from './actions/event_actions'; -/* eslint-disable */ // You can comment out the modules you don't need -// import DebugHandler from './third_party/debug'; -import GoogleAnalyticsHandler from './third_party/ga'; -import RavenHandler from './third_party/raven'; -import IntercomHandler from './third_party/intercom'; -import NotificationsHandler from './third_party/notifications'; -import FacebookHandler from './third_party/facebook'; -/* eslint-enable */ +// import DebugHandler from './third_party/debug_handler'; +import FacebookHandler from './third_party/facebook_handler'; +import GoogleAnalyticsHandler from './third_party/ga_handler'; +import IntercomHandler from './third_party/intercom_handler'; +import NotificationsHandler from './third_party/notifications_handler'; +import RavenHandler from './third_party/raven_handler'; -initLogging(); -let headers = { - 'Accept': 'application/json', - 'Content-Type': 'application/json' -}; - -requests.defaults({ - urlMap: ApiUrls, - http: { - headers: headers, - credentials: 'include' - } -}); - -class AppGateway { +const AppGateway = { start() { - let settings; - let subdomain = getSubdomain(); - try { - settings = getSubdomainSettings(subdomain); + const subdomain = getSubdomain(); + const settings = getSubdomainSettings(subdomain); + AppConstants.whitelabel = settings; updateApiUrls(settings.type, subdomain); this.load(settings); @@ -62,28 +44,25 @@ class AppGateway { // if there are no matching subdomains, we're routing // to the default frontend console.logGlobal(err); - this.load(); + this.load(getDefaultSubdomainSettings()); } - } + }, load(settings) { - let type = 'default'; - let subdomain = 'www'; + const { subdomain, type } = settings; let redirectRoute = (); - if (settings) { - type = settings.type; - subdomain = settings.subdomain; - } + if (subdomain) { + // Some whitelabels have landing pages so we should not automatically redirect from / to /collection. + // Only www and cc do not have a landing page. + if (subdomain !== 'cc') { + redirectRoute = null; + } - // www and cc do not have a landing page - if(subdomain && subdomain !== 'cc') { - redirectRoute = null; + // Adds a client specific class to the body for whitelabel styling + window.document.body.classList.add('client--' + subdomain); } - // Adds a client specific class to the body for whitelabel styling - window.document.body.classList.add('client--' + subdomain); - // Send the applicationWillBoot event to the third-party stores EventActions.applicationWillBoot(settings); @@ -101,8 +80,21 @@ class AppGateway { // Send the applicationDidBoot event to the third-party stores EventActions.applicationDidBoot(settings); } -} +}; -let ag = new AppGateway(); -ag.start(); +// Initialize pre-start components +initLogging(); +requests.defaults({ + urlMap: ApiUrls, + http: { + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + credentials: 'include' + } +}); + +// And bootstrap app +AppGateway.start(); diff --git a/js/components/app_base.js b/js/components/app_base.js new file mode 100644 index 00000000..3d14fae5 --- /dev/null +++ b/js/components/app_base.js @@ -0,0 +1,90 @@ +'use strict'; + +import React from 'react'; +import classNames from 'classnames'; +import { History } from 'react-router'; + +import UserActions from '../actions/user_actions'; +import UserStore from '../stores/user_store'; + +import WhitelabelActions from '../actions/whitelabel_actions'; +import WhitelabelStore from '../stores/whitelabel_store'; + +import GlobalNotification from './global_notification'; + +import AppConstants from '../constants/application_constants'; + +import { mergeOptions } from '../utils/general_utils'; + + +export default function AppBase(App) { + return React.createClass({ + displayName: 'AppBase', + + propTypes: { + children: React.PropTypes.element.isRequired, + history: React.PropTypes.object.isRequired, + location: React.PropTypes.object.isRequired, + routes: React.PropTypes.arrayOf(React.PropTypes.object).isRequired + }, + + getInitialState() { + return mergeOptions( + UserStore.getState(), + WhitelabelStore.getState() + ); + }, + + mixins: [History], + + componentDidMount() { + UserStore.listen(this.onChange); + WhitelabelStore.listen(this.onChange); + + UserActions.fetchCurrentUser(); + WhitelabelActions.fetchWhitelabel(); + + this.history.locationQueue.push(this.props.location); + }, + + componentWillReceiveProps(nextProps) { + const { locationQueue } = this.history; + locationQueue.unshift(nextProps.location); + + // Limit the number of locations to keep in memory to avoid too much memory usage + if (locationQueue.length > AppConstants.locationThreshold) { + locationQueue.length = AppConstants.locationThreshold; + } + }, + + componentWillUnmount() { + UserStore.unlisten(this.onChange); + WhitelabelActions.unlisten(this.onChange); + }, + + onChange(state) { + this.setState(state); + }, + + render() { + const { routes } = this.props; + const { currentUser, whitelabel } = this.state; + + // The second element of the routes prop given to us by react-router is always the + // active second-level component object (ie. after App). + const activeRoute = routes[1]; + + return ( +
    + + + + ); + } + }); +}; diff --git a/js/components/app_route_wrapper.js b/js/components/app_route_wrapper.js index 0abbcb32..d680faeb 100644 --- a/js/components/app_route_wrapper.js +++ b/js/components/app_route_wrapper.js @@ -24,7 +24,7 @@ const AppRouteWrapper = React.createClass({ } return ( -
    +
    {childrenWithProps}
    ); diff --git a/js/components/ascribe_accordion_list/accordion_list.js b/js/components/ascribe_accordion_list/accordion_list.js index f1967edb..1ce113cb 100644 --- a/js/components/ascribe_accordion_list/accordion_list.js +++ b/js/components/ascribe_accordion_list/accordion_list.js @@ -8,9 +8,10 @@ import { getLangText } from '../../utils/lang_utils'; let AccordionList = React.createClass({ propTypes: { - className: React.PropTypes.string, children: React.PropTypes.arrayOf(React.PropTypes.element).isRequired, - loadingElement: React.PropTypes.element, + loadingElement: React.PropTypes.element.isRequired, + + className: React.PropTypes.string, count: React.PropTypes.number, itemList: React.PropTypes.arrayOf(React.PropTypes.object), search: React.PropTypes.string, @@ -24,7 +25,7 @@ let AccordionList = React.createClass({ render() { const { search } = this.props; - if(this.props.itemList && this.props.itemList.length > 0) { + if (this.props.itemList && this.props.itemList.length > 0) { return (
    {this.props.children} diff --git a/js/components/ascribe_accordion_list/accordion_list_item_edition_widget.js b/js/components/ascribe_accordion_list/accordion_list_item_edition_widget.js index 27657e04..2bb8b2d0 100644 --- a/js/components/ascribe_accordion_list/accordion_list_item_edition_widget.js +++ b/js/components/ascribe_accordion_list/accordion_list_item_edition_widget.js @@ -19,9 +19,10 @@ import { getLangText } from '../../utils/lang_utils'; let AccordionListItemEditionWidget = React.createClass({ propTypes: { - className: React.PropTypes.string, piece: React.PropTypes.object.isRequired, toggleCreateEditionsDialog: React.PropTypes.func.isRequired, + + className: React.PropTypes.string, onPollingSuccess: React.PropTypes.func }, @@ -50,14 +51,15 @@ let AccordionListItemEditionWidget = React.createClass({ * Calls the store to either show or hide the editionListTable */ toggleTable() { - let pieceId = this.props.piece.id; - let isEditionListOpen = this.state.isEditionListOpenForPieceId[pieceId] ? this.state.isEditionListOpenForPieceId[pieceId].show : false; + const { piece: { id: pieceId } } = this.props; + const { filterBy, isEditionListOpenForPieceId } = this.state; + const isEditionListOpen = isEditionListOpenForPieceId[pieceId] ? isEditionListOpenForPieceId[pieceId].show : false; - if(isEditionListOpen) { + if (isEditionListOpen) { EditionListActions.toggleEditionList(pieceId); } else { EditionListActions.toggleEditionList(pieceId); - EditionListActions.fetchEditionList(pieceId, null, null, null, null, this.state.filterBy); + EditionListActions.fetchEditionList({ pieceId, filterBy }); } }, diff --git a/js/components/ascribe_accordion_list/accordion_list_item_table_editions.js b/js/components/ascribe_accordion_list/accordion_list_item_table_editions.js index 586b75a2..3b0bb02e 100644 --- a/js/components/ascribe_accordion_list/accordion_list_item_table_editions.js +++ b/js/components/ascribe_accordion_list/accordion_list_item_table_editions.js @@ -66,20 +66,28 @@ let AccordionListItemTableEditions = React.createClass({ }, filterSelectedEditions() { - let selectedEditions = this.state.editionList[this.props.parentId] - .filter((edition) => edition.selected); - return selectedEditions; + return this.state + .editionList[this.props.parentId] + .filter((edition) => edition.selected); }, loadFurtherEditions() { + const { parentId: pieceId } = this.props; + const { page, pageSize, orderBy, orderAsc, filterBy } = this.state.editionList[pieceId]; + // trigger loading animation this.setState({ showMoreLoading: true }); - let editionList = this.state.editionList[this.props.parentId]; - EditionListActions.fetchEditionList(this.props.parentId, editionList.page + 1, editionList.pageSize, - editionList.orderBy, editionList.orderAsc, editionList.filterBy); + EditionListActions.fetchEditionList({ + pieceId, + pageSize, + orderBy, + orderAsc, + filterBy, + page: page + 1 + }); }, render() { const { className, parentId } = this.props; diff --git a/js/components/ascribe_accordion_list/accordion_list_item_thumbnail_placeholder.js b/js/components/ascribe_accordion_list/accordion_list_item_thumbnail_placeholder.js index 37c98371..8000affd 100644 --- a/js/components/ascribe_accordion_list/accordion_list_item_thumbnail_placeholder.js +++ b/js/components/ascribe_accordion_list/accordion_list_item_thumbnail_placeholder.js @@ -1,4 +1,4 @@ -'use strict' +'use strict'; import React from 'react'; diff --git a/js/components/ascribe_accordion_list/accordion_list_item_wallet.js b/js/components/ascribe_accordion_list/accordion_list_item_wallet.js index a3266be9..c586b926 100644 --- a/js/components/ascribe_accordion_list/accordion_list_item_wallet.js +++ b/js/components/ascribe_accordion_list/accordion_list_item_wallet.js @@ -31,12 +31,12 @@ let AccordionListItemWallet = React.createClass({ content: React.PropTypes.object.isRequired, whitelabel: React.PropTypes.object.isRequired, - className: React.PropTypes.string, - thumbnailPlaceholder: React.PropTypes.func, children: React.PropTypes.oneOfType([ React.PropTypes.arrayOf(React.PropTypes.element), React.PropTypes.element - ]) + ]), + className: React.PropTypes.string, + thumbnailPlaceholder: React.PropTypes.func }, getInitialState() { @@ -67,10 +67,12 @@ let AccordionListItemWallet = React.createClass({ delay={500} placement="left" overlay={{getLangText('You have actions pending')}}> - - ); + + + ); + } else { + return null; } - return null; }, toggleCreateEditionsDialog() { @@ -86,8 +88,9 @@ let AccordionListItemWallet = React.createClass({ }, onPollingSuccess(pieceId) { - PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, - this.state.orderBy, this.state.orderAsc, this.state.filterBy); + const { filterBy, orderAsc, orderBy, page, pageSize, search } = this.state; + + PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy }); EditionListActions.toggleEditionList(pieceId); const notification = new GlobalNotificationModel(getLangText('Editions successfully created'), 'success', 10000); diff --git a/js/components/ascribe_app.js b/js/components/ascribe_app.js index f7000aca..863bffc0 100644 --- a/js/components/ascribe_app.js +++ b/js/components/ascribe_app.js @@ -2,60 +2,28 @@ import React from 'react'; -import UserActions from '../actions/user_actions'; -import UserStore from '../stores/user_store'; - -import WhitelabelActions from '../actions/whitelabel_actions'; -import WhitelabelStore from '../stores/whitelabel_store'; - +import AppBase from './app_base'; import AppRouteWrapper from './app_route_wrapper'; -import Header from './header'; import Footer from './footer'; -import GlobalNotification from './global_notification'; - -import { mergeOptions } from '../utils/general_utils'; +import Header from './header'; let AscribeApp = React.createClass({ propTypes: { + activeRoute: React.PropTypes.object.isRequired, + children: React.PropTypes.element.isRequired, routes: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, - children: React.PropTypes.oneOfType([ - React.PropTypes.arrayOf(React.PropTypes.element), - React.PropTypes.element - ]) - }, - - getInitialState() { - return mergeOptions( - UserStore.getState(), - WhitelabelStore.getState() - ); - }, - - componentDidMount() { - UserStore.listen(this.onChange); - WhitelabelStore.listen(this.onChange); - - UserActions.fetchCurrentUser(); - WhitelabelActions.fetchWhitelabel(); - }, - - componentWillUnmount() { - UserStore.unlisten(this.onChange); - WhitelabelActions.unlisten(this.onChange); - }, - - onChange(state) { - this.setState(state); + // Provided from AppBase + currentUser: React.PropTypes.object, + whitelabel: React.PropTypes.object }, render() { - const { children, routes } = this.props; - const { currentUser, whitelabel } = this.state; + const { activeRoute, children, currentUser, routes, whitelabel } = this.props; return ( -
    +
    -
    - - +
    ); } }); -export default AscribeApp; +export default AppBase(AscribeApp); diff --git a/js/components/ascribe_buttons/acls/acl_button.js b/js/components/ascribe_buttons/acls/acl_button.js index 2dc21d31..df349152 100644 --- a/js/components/ascribe_buttons/acls/acl_button.js +++ b/js/components/ascribe_buttons/acls/acl_button.js @@ -14,7 +14,7 @@ import AppConstants from '../../../constants/application_constants'; import { AclInformationText } from '../../../constants/acl_information_text'; -export default function ({ action, displayName, title, tooltip }) { +export default function AclButton({ action, displayName, title, tooltip }) { if (AppConstants.aclList.indexOf(action) < 0) { console.warn('Your specified aclName did not match a an acl class.'); } diff --git a/js/components/ascribe_buttons/create_editions_button.js b/js/components/ascribe_buttons/create_editions_button.js index 08fb76ce..c78b0d63 100644 --- a/js/components/ascribe_buttons/create_editions_button.js +++ b/js/components/ascribe_buttons/create_editions_button.js @@ -28,6 +28,12 @@ let CreateEditionsButton = React.createClass({ EditionListStore.listen(this.onChange); }, + componentDidUpdate() { + if(this.props.piece.num_editions === 0 && typeof this.state.pollingIntervalIndex === 'undefined') { + this.startPolling(); + } + }, + componentWillUnmount() { EditionListStore.unlisten(this.onChange); clearInterval(this.state.pollingIntervalIndex); @@ -37,28 +43,24 @@ let CreateEditionsButton = React.createClass({ this.setState(state); }, - componentDidUpdate() { - if(this.props.piece.num_editions === 0 && typeof this.state.pollingIntervalIndex === 'undefined') { - this.startPolling(); - } - }, - startPolling() { // start polling until editions are defined let pollingIntervalIndex = setInterval(() => { // requests, will try to merge the filterBy parameter with other parameters (mergeOptions). // Therefore it can't but null but instead has to be an empty object - EditionListActions.fetchEditionList(this.props.piece.id, null, null, null, null, {}) - .then((res) => { - - clearInterval(this.state.pollingIntervalIndex); - this.props.onPollingSuccess(this.props.piece.id, res.editions[0].num_editions); - - }) - .catch((err) => { - /* Ignore and keep going */ - }); + EditionListActions + .fetchEditionList({ + pieceId: this.props.piece.id, + filterBy: {} + }) + .then((res) => { + clearInterval(this.state.pollingIntervalIndex); + this.props.onPollingSuccess(this.props.piece.id, res.editions[0].num_editions); + }) + .catch((err) => { + /* Ignore and keep going */ + }); }, 5000); this.setState({ diff --git a/js/components/ascribe_detail/edition.js b/js/components/ascribe_detail/edition.js index deb275ec..f2110e10 100644 --- a/js/components/ascribe_detail/edition.js +++ b/js/components/ascribe_detail/edition.js @@ -74,7 +74,7 @@ let Edition = React.createClass({

    {edition.title}

    - +
    diff --git a/js/components/ascribe_detail/edition_action_panel.js b/js/components/ascribe_detail/edition_action_panel.js index 2d9c9a26..dd5c117b 100644 --- a/js/components/ascribe_detail/edition_action_panel.js +++ b/js/components/ascribe_detail/edition_action_panel.js @@ -81,9 +81,10 @@ let EditionActionPanel = React.createClass({ }, refreshCollection() { - PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, - this.state.orderBy, this.state.orderAsc, this.state.filterBy); - EditionListActions.refreshEditionList({pieceId: this.props.edition.parent}); + const { filterBy, orderAsc, orderBy, page, pageSize, search } = this.state; + + PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy }); + EditionListActions.refreshEditionList({ pieceId: this.props.edition.parent }); }, handleSuccess(response) { diff --git a/js/components/ascribe_detail/edition_container.js b/js/components/ascribe_detail/edition_container.js index d26bd529..b9696d0a 100644 --- a/js/components/ascribe_detail/edition_container.js +++ b/js/components/ascribe_detail/edition_container.js @@ -37,31 +37,28 @@ let EditionContainer = React.createClass({ mixins: [History, ReactError], getInitialState() { - return EditionStore.getState(); + return EditionStore.getInitialState(); }, componentDidMount() { EditionStore.listen(this.onChange); - // Every time we're entering the edition detail page, - // just reset the edition that is saved in the edition store - // as it will otherwise display wrong/old data once the user loads - // the edition detail a second time - EditionActions.flushEdition(); - EditionActions.fetchEdition(this.props.params.editionId); + this.loadEdition(); }, // This is done to update the container when the user clicks on the prev or next // button to update the URL parameter (and therefore to switch pieces) componentWillReceiveProps(nextProps) { if (this.props.params.editionId !== nextProps.params.editionId) { - EditionActions.fetchEdition(this.props.params.editionId); + EditionActions.flushEdition(); + this.loadEdition(nextProps.params.editionId); } }, componentDidUpdate() { - const { editionMeta } = this.state; - if (editionMeta.err && editionMeta.err.json && editionMeta.err.json.status === 404) { + const { err: editionErr } = this.state.editionMeta; + + if (editionErr && editionErr.json && editionErr.json.status === 404) { this.throws(new ResourceNotFoundError(getLangText("Oops, the edition you're looking for doesn't exist."))); } }, @@ -75,12 +72,16 @@ let EditionContainer = React.createClass({ this.setState(state); }, + loadEdition(editionId = this.props.params.editionId) { + EditionActions.fetchEdition(editionId); + }, + render() { const { actionPanelButtonListType, currentUser, furtherDetailsType, whitelabel } = this.props; const { edition, coaMeta } = this.state; - if (Object.keys(edition).length && edition.id) { - setDocumentTitle([edition.artist_name, edition.title].join(', ')); + if (edition.id) { + setDocumentTitle(`${edition.artist_name}, ${edition.title}`); return ( EditionActions.fetchEdition(this.props.params.editionId)} + loadEdition={this.loadEdition} whitelabel={whitelabel} /> ); } else { diff --git a/js/components/ascribe_detail/further_details.js b/js/components/ascribe_detail/further_details.js index c178fb93..b1d9c637 100644 --- a/js/components/ascribe_detail/further_details.js +++ b/js/components/ascribe_detail/further_details.js @@ -5,25 +5,27 @@ import React from 'react'; import Row from 'react-bootstrap/lib/Row'; import Col from 'react-bootstrap/lib/Col'; -import Form from './../ascribe_forms/form'; - -import PieceExtraDataForm from './../ascribe_forms/form_piece_extradata'; - import GlobalNotificationModel from '../../models/global_notification_model'; import GlobalNotificationActions from '../../actions/global_notification_actions'; import FurtherDetailsFileuploader from './further_details_fileuploader'; +import Form from './../ascribe_forms/form'; +import PieceExtraDataForm from './../ascribe_forms/form_piece_extradata'; + import { formSubmissionValidation } from '../ascribe_uploader/react_s3_fine_uploader_utils'; +import { getLangText } from '../../utils/lang_utils'; + let FurtherDetails = React.createClass({ propTypes: { + pieceId: React.PropTypes.number.isRequired, + editable: React.PropTypes.bool, - pieceId: React.PropTypes.number, extraData: React.PropTypes.object, + handleSuccess: React.PropTypes.func, otherData: React.PropTypes.arrayOf(React.PropTypes.object), - handleSuccess: React.PropTypes.func }, getInitialState() { @@ -32,13 +34,18 @@ let FurtherDetails = React.createClass({ }; }, - showNotification(){ - this.props.handleSuccess(); - let notification = new GlobalNotificationModel('Details updated', 'success'); + showNotification() { + const { handleSuccess } = this.props; + + if (typeof handleSucess === 'function') { + handleSuccess(); + } + + const notification = new GlobalNotificationModel(getLangText('Details updated'), 'success'); GlobalNotificationActions.appendGlobalNotification(notification); }, - submitFile(file){ + submitFile(file) { this.setState({ otherDataKey: file.key }); @@ -51,40 +58,42 @@ let FurtherDetails = React.createClass({ }, render() { + const { editable, extraData, otherData, pieceId } = this.props; + return ( + pieceId={pieceId} /> + pieceId={pieceId} /> + pieceId={pieceId} />
    diff --git a/js/components/ascribe_detail/further_details_fileuploader.js b/js/components/ascribe_detail/further_details_fileuploader.js index acdbe9e0..61e0724f 100644 --- a/js/components/ascribe_detail/further_details_fileuploader.js +++ b/js/components/ascribe_detail/further_details_fileuploader.js @@ -8,6 +8,7 @@ import ReactS3FineUploader from './../ascribe_uploader/react_s3_fine_uploader'; import ApiUrls from '../../constants/api_urls'; import AppConstants from '../../constants/application_constants'; +import { validationTypes } from '../../constants/uploader_constants'; import { getCookie } from '../../utils/fetch_api_utils'; import { getLangText } from '../../utils/lang_utils'; @@ -15,21 +16,26 @@ import { getLangText } from '../../utils/lang_utils'; let FurtherDetailsFileuploader = React.createClass({ propTypes: { + pieceId: React.PropTypes.number.isRequired, + + areAssetsDownloadable: React.PropTypes.bool, + editable: React.PropTypes.bool, + isReadyForFormSubmission: React.PropTypes.func, label: React.PropTypes.string, - pieceId: React.PropTypes.number, + multiple: React.PropTypes.bool, otherData: React.PropTypes.arrayOf(React.PropTypes.object), + onValidationFailed: React.PropTypes.func, setIsUploadReady: React.PropTypes.func, submitFile: React.PropTypes.func, - onValidationFailed: React.PropTypes.func, - isReadyForFormSubmission: React.PropTypes.func, - editable: React.PropTypes.bool, - multiple: React.PropTypes.bool + validation: ReactS3FineUploader.propTypes.validation }, getDefaultProps() { return { + areAssetsDownloadable: true, label: getLangText('Additional files'), - multiple: false + multiple: false, + validation: validationTypes.additionalData }; }, @@ -59,7 +65,7 @@ let FurtherDetailsFileuploader = React.createClass({ url: ApiUrls.blob_otherdatas, pieceId: this.props.pieceId }} - validation={AppConstants.fineUploader.validation.additionalData} + validation={this.props.validation} submitFile={this.props.submitFile} onValidationFailed={this.props.onValidationFailed} setIsUploadReady={this.props.setIsUploadReady} @@ -91,7 +97,7 @@ let FurtherDetailsFileuploader = React.createClass({ 'X-CSRFToken': getCookie(AppConstants.csrftoken) } }} - areAssetsDownloadable={true} + areAssetsDownloadable={this.props.areAssetsDownloadable} areAssetsEditable={this.props.editable} multiple={this.props.multiple} /> diff --git a/js/components/ascribe_detail/media_container.js b/js/components/ascribe_detail/media_container.js index 8ea374b3..1e9ba0a1 100644 --- a/js/components/ascribe_detail/media_container.js +++ b/js/components/ascribe_detail/media_container.js @@ -14,7 +14,9 @@ import CollapsibleButton from './../ascribe_collapsible/collapsible_button'; import AclProxy from '../acl_proxy'; -import { getLangText } from '../../utils/lang_utils.js'; +import { getLangText } from '../../utils/lang_utils'; +import { extractFileExtensionFromString } from '../../utils/file_utils'; + const EMBED_IFRAME_HEIGHT = { video: 315, @@ -68,6 +70,16 @@ let MediaContainer = React.createClass({ // content was registered by the current user. const didUserRegisterContent = currentUser && (currentUser.username === content.user_registered); + // We want to show the file's extension as a label of the download button. + // We can however not only use `extractFileExtensionFromString` on the url for that + // as files might be saved on S3 without a file extension which leads + // `extractFileExtensionFromString` to extract everything starting from the top level + // domain: e.g. '.net/live/'. + // Therefore, we extract the file's name (last part of url, separated with a slash) + // and try to extract the file extension from there. + const fileName = content.digital_work.url.split('/').pop(); + const fileExtension = extractFileExtensionFromString(fileName); + let thumbnail = content.thumbnail.thumbnail_sizes && content.thumbnail.thumbnail_sizes['600x600'] ? content.thumbnail.thumbnail_sizes['600x600'] : content.thumbnail.url_safe; let mimetype = content.digital_work.mime; @@ -124,7 +136,11 @@ let MediaContainer = React.createClass({ className="ascribe-margin-1px" href={content.digital_work.url} target="_blank"> - {getLangText('Download')} .{mimetype} + {/* + If it turns out that `fileExtension` is an empty string, we're just + using the label 'file'. + */} + {getLangText('Download')} .{fileExtension || 'file'} {embed} diff --git a/js/components/ascribe_detail/piece.js b/js/components/ascribe_detail/piece.js index 58648c7b..31cf0574 100644 --- a/js/components/ascribe_detail/piece.js +++ b/js/components/ascribe_detail/piece.js @@ -27,7 +27,7 @@ let Piece = React.createClass({ }, updatePiece() { - return PieceActions.fetchOne(this.props.piece.id); + return PieceActions.fetchPiece(this.props.piece.id); }, render() { diff --git a/js/components/ascribe_detail/piece_container.js b/js/components/ascribe_detail/piece_container.js index 93bbd90b..30dc2566 100644 --- a/js/components/ascribe_detail/piece_container.js +++ b/js/components/ascribe_detail/piece_container.js @@ -71,7 +71,7 @@ let PieceContainer = React.createClass({ getInitialState() { return mergeOptions( PieceListStore.getState(), - PieceStore.getState(), + PieceStore.getInitialState(), { showCreateEditionsDialog: false } @@ -82,18 +82,23 @@ let PieceContainer = React.createClass({ PieceListStore.listen(this.onChange); PieceStore.listen(this.onChange); - // Every time we enter the piece detail page, just reset the piece - // store as it will otherwise display wrong/old data once the user loads - // the piece detail a second time - PieceActions.updatePiece({}); - this.loadPiece(); }, - componentDidUpdate() { - const { pieceError } = this.state; + // This is done to update the container when the user clicks on the prev or next + // button to update the URL parameter (and therefore to switch pieces) or + // when the user clicks on a notification while being in another piece view + componentWillReceiveProps(nextProps) { + if (this.props.params.pieceId !== nextProps.params.pieceId) { + PieceActions.flushPiece(); + this.loadPiece(nextProps.params.pieceId); + } + }, - if (pieceError && pieceError.status === 404) { + componentDidUpdate() { + const { err: pieceErr } = this.state.pieceMeta; + + if (pieceErr && pieceErr.json && pieceErr.json.status === 404) { this.throws(new ResourceNotFoundError(getLangText("Oops, the piece you're looking for doesn't exist."))); } }, @@ -115,8 +120,7 @@ let PieceContainer = React.createClass({ ALSO, WE ENABLED THE LOAN BUTTON FOR IKONOTV TO LET THEM LOAN ON A PIECE LEVEL */ - if(state && state.piece && state.piece.acl && typeof state.piece.acl.acl_loan !== 'undefined') { - + if (state && state.piece && state.piece.acl && typeof state.piece.acl.acl_loan !== 'undefined') { let pieceState = mergeOptions({}, state.piece); pieceState.acl.acl_loan = false; this.setState({ @@ -128,11 +132,10 @@ let PieceContainer = React.createClass({ } }, - loadPiece() { - PieceActions.fetchOne(this.props.params.pieceId); + loadPiece(pieceId = this.props.params.pieceId) { + PieceActions.fetchPiece(pieceId); }, - toggleCreateEditionsDialog() { this.setState({ showCreateEditionsDialog: !this.state.showCreateEditionsDialog @@ -140,43 +143,47 @@ let PieceContainer = React.createClass({ }, handleEditionCreationSuccess() { - PieceActions.updateProperty({key: 'num_editions', value: 0}); - PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, - this.state.orderBy, this.state.orderAsc, this.state.filterBy); + const { filterBy, orderAsc, orderBy, page, pageSize, search } = this.state; + + PieceActions.updateProperty({ key: 'num_editions', value: 0 }); + PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy }); + this.toggleCreateEditionsDialog(); }, handleDeleteSuccess(response) { - PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, - this.state.orderBy, this.state.orderAsc, this.state.filterBy); + const { filterBy, orderAsc, orderBy, page, pageSize, search } = this.state; + + PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy }); // since we're deleting a piece, we just need to close // all editions dialogs and not reload them EditionListActions.closeAllEditionLists(); EditionListActions.clearAllEditionSelections(); - let notification = new GlobalNotificationModel(response.notification, 'success'); + const notification = new GlobalNotificationModel(response.notification, 'success'); GlobalNotificationActions.appendGlobalNotification(notification); this.history.push('/collection'); }, getCreateEditionsDialog() { - if(this.state.piece.num_editions < 1 && this.state.showCreateEditionsDialog) { + if (this.state.piece.num_editions < 1 && this.state.showCreateEditionsDialog) { return (
    -
    +
    ); } else { - return (
    ); + return (
    ); } }, handlePollingSuccess(pieceId, numEditions) { + const { filterBy, orderAsc, orderBy, page, pageSize, search } = this.state; // we need to refresh the num_editions property of the actual piece we're looking at PieceActions.updateProperty({ @@ -188,28 +195,28 @@ let PieceContainer = React.createClass({ // btw.: It's not sufficient to just set num_editions to numEditions, since a single accordion // list item also uses the firstEdition property which we can only get from the server in that case. // Therefore we need to at least refetch the changed piece from the server or on our case simply all - PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, - this.state.orderBy, this.state.orderAsc, this.state.filterBy); + PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy }); - let notification = new GlobalNotificationModel('Editions successfully created', 'success', 10000); + const notification = new GlobalNotificationModel(getLangText('Editions successfully created'), 'success', 10000); GlobalNotificationActions.appendGlobalNotification(notification); }, getId() { - return {'id': this.state.piece.id}; + return { 'id': this.state.piece.id }; }, getActions() { const { piece } = this.state; const { currentUser } = this.props; - if (piece && piece.notifications && piece.notifications.length > 0) { + if (piece.notifications && piece.notifications.length > 0) { return ( ); + pieceOrEditions={piece} /> + ); } else { return ( + piece={piece} /> + aclObject={piece.acl} /> @@ -253,8 +260,8 @@ let PieceContainer = React.createClass({ const { currentUser, furtherDetailsType: FurtherDetailsType } = this.props; const { piece } = this.state; - if (piece && piece.id) { - setDocumentTitle([piece.artist_name, piece.title].join(', ')); + if (piece.id) { + setDocumentTitle(`${piece.artist_name}, ${piece.title}`); return (

    {piece.title}

    - + {piece.num_editions > 0 ? : null}
    @@ -272,7 +279,7 @@ let PieceContainer = React.createClass({ } subheader={
    - +
    diff --git a/js/components/ascribe_forms/create_editions_form.js b/js/components/ascribe_forms/create_editions_form.js index 1e8ac23e..b2bf3c1c 100644 --- a/js/components/ascribe_forms/create_editions_form.js +++ b/js/components/ascribe_forms/create_editions_form.js @@ -19,7 +19,7 @@ let CreateEditionsForm = React.createClass({ pieceId: React.PropTypes.number }, - getFormData(){ + getFormData() { return { piece_id: parseInt(this.props.pieceId, 10) }; @@ -58,11 +58,12 @@ let CreateEditionsForm = React.createClass({ + min={1} + max={100} /> ); } }); -export default CreateEditionsForm; \ No newline at end of file +export default CreateEditionsForm; diff --git a/js/components/ascribe_forms/form.js b/js/components/ascribe_forms/form.js index 91d00f65..fcc2f799 100644 --- a/js/components/ascribe_forms/form.js +++ b/js/components/ascribe_forms/form.js @@ -156,7 +156,7 @@ let Form = React.createClass({ for(let ref in this.refs) { if(this.refs[ref] && typeof this.refs[ref].handleSuccess === 'function'){ - this.refs[ref].handleSuccess(); + this.refs[ref].handleSuccess(response); } } this.setState({ @@ -205,16 +205,15 @@ let Form = React.createClass({ }, getButtons() { - if (this.state.submitted){ + if (this.state.submitted) { return this.props.spinner; } - if (this.props.buttons){ + if (this.props.buttons !== undefined) { return this.props.buttons; } - let buttons = null; - if (this.state.edited && !this.props.disabled){ - buttons = ( + if (this.state.edited && !this.props.disabled) { + return (

    ); - + } else { + return null; } - return buttons; }, getErrors() { diff --git a/js/components/ascribe_forms/form_consign.js b/js/components/ascribe_forms/form_consign.js index a28d2cff..e2e83567 100644 --- a/js/components/ascribe_forms/form_consign.js +++ b/js/components/ascribe_forms/form_consign.js @@ -123,8 +123,7 @@ let ConsignForm = React.createClass({ + className="ascribe-property-collapsible-toggle"> diff --git a/js/components/ascribe_forms/form_copyright_association.js b/js/components/ascribe_forms/form_copyright_association.js index c378ddba..124a980a 100644 --- a/js/components/ascribe_forms/form_copyright_association.js +++ b/js/components/ascribe_forms/form_copyright_association.js @@ -48,8 +48,7 @@ let CopyrightAssociationForm = React.createClass({ + label={getLangText('Copyright Association')}> @@ -210,8 +210,7 @@ let LoanForm = React.createClass({ + className="ascribe-property-collapsible-toggle"> diff --git a/js/components/ascribe_forms/form_piece_extradata.js b/js/components/ascribe_forms/form_piece_extradata.js index f6ee4177..6a475fb6 100644 --- a/js/components/ascribe_forms/form_piece_extradata.js +++ b/js/components/ascribe_forms/form_piece_extradata.js @@ -13,43 +13,48 @@ import InputTextAreaToggable from './input_textarea_toggable'; let PieceExtraDataForm = React.createClass({ propTypes: { - pieceId: React.PropTypes.number, + name: React.PropTypes.string.isRequired, + pieceId: React.PropTypes.number.isRequired, + + convertLinks: React.PropTypes.bool, + editable: React.PropTypes.bool, extraData: React.PropTypes.object, handleSuccess: React.PropTypes.func, - name: React.PropTypes.string, - title: React.PropTypes.string, - editable: React.PropTypes.bool + title: React.PropTypes.string }, getFormData() { - let extradata = {}; - extradata[this.props.name] = this.refs.form.refs[this.props.name].state.value; return { - extradata: extradata, + extradata: { + [this.props.name]: this.refs.form.refs[this.props.name].state.value + }, piece_id: this.props.pieceId }; }, - + render() { - let defaultValue = this.props.extraData[this.props.name] || ''; - if (defaultValue.length === 0 && !this.props.editable){ + const { convertLinks, editable, extraData, handleSuccess, name, pieceId, title } = this.props; + const defaultValue = (extraData && extraData[name]) || null; + + if (!defaultValue && !editable) { return null; } - let url = requests.prepareUrl(ApiUrls.piece_extradata, {piece_id: this.props.pieceId}); + return (
    + handleSuccess={handleSuccess} + url={requests.prepareUrl(ApiUrls.piece_extradata, { piece_id: pieceId })}> + name={name} + label={title}>
    diff --git a/js/components/ascribe_forms/form_register_piece.js b/js/components/ascribe_forms/form_register_piece.js index 35059d5d..69a3b6b7 100644 --- a/js/components/ascribe_forms/form_register_piece.js +++ b/js/components/ascribe_forms/form_register_piece.js @@ -5,12 +5,16 @@ import React from 'react'; import Form from './form'; import Property from './property'; import InputFineUploader from './input_fineuploader'; -import UploadButton from '../ascribe_uploader/ascribe_upload_button/upload_button'; + import FormSubmitButton from '../ascribe_buttons/form_submit_button'; +import UploadButton from '../ascribe_uploader/ascribe_upload_button/upload_button'; + +import AscribeSpinner from '../ascribe_spinner'; + import ApiUrls from '../../constants/api_urls'; import AppConstants from '../../constants/application_constants'; -import AscribeSpinner from '../ascribe_spinner'; +import { validationParts, validationTypes } from '../../constants/uploader_constants'; import { getLangText } from '../../utils/lang_utils'; import { formSubmissionValidation } from '../ascribe_uploader/react_s3_fine_uploader_utils'; @@ -164,7 +168,7 @@ let RegisterPieceForm = React.createClass({ createBlobRoutine={{ url: ApiUrls.blob_digitalworks }} - validation={AppConstants.fineUploader.validation.registerWork} + validation={validationTypes.registerWork} setIsUploadReady={this.setIsUploadReady('digitalWorkKeyReady')} isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile} isFineUploaderActive={isFineUploaderActive} @@ -190,9 +194,9 @@ let RegisterPieceForm = React.createClass({ fileClass: 'thumbnail' }} validation={{ - itemLimit: AppConstants.fineUploader.validation.workThumbnail.itemLimit, - sizeLimit: AppConstants.fineUploader.validation.workThumbnail.sizeLimit, - allowedExtensions: ['png', 'jpg', 'jpeg', 'gif'] + itemLimit: validationTypes.workThumbnail.itemLimit, + sizeLimit: validationTypes.workThumbnail.sizeLimit, + allowedExtensions: validationParts.allowedExtensions.images }} setIsUploadReady={this.setIsUploadReady('thumbnailKeyReady')} fileClassToUpload={{ @@ -200,9 +204,7 @@ let RegisterPieceForm = React.createClass({ plural: getLangText('Select representative images') }} isFineUploaderActive={isFineUploaderActive} - disabled={!isFineUploaderEditable} - enableLocalHashing={enableLocalHashing} - uploadMethod={location.query.method} /> + disabled={!isFineUploaderEditable} /> + expanded={false}> {getLangText('Appendix')} {/* We're using disabled on a form here as PropertyCollapsible currently does not support the disabled + overrideForm functionality */} diff --git a/js/components/ascribe_forms/form_signup.js b/js/components/ascribe_forms/form_signup.js index 6faa6cf8..69fadd36 100644 --- a/js/components/ascribe_forms/form_signup.js +++ b/js/components/ascribe_forms/form_signup.js @@ -121,8 +121,7 @@ let SignupForm = React.createClass({ {children} + className="ascribe-property-collapsible-toggle"> {' ' + getLangText('I agree to the Terms of Service of ascribe') + ' '} diff --git a/js/components/ascribe_forms/form_submit_to_prize.js b/js/components/ascribe_forms/form_submit_to_prize.js index 5818a9ce..43f28e2a 100644 --- a/js/components/ascribe_forms/form_submit_to_prize.js +++ b/js/components/ascribe_forms/form_submit_to_prize.js @@ -66,8 +66,7 @@ let PieceSubmitToPrizeForm = React.createClass({ + className="ascribe-property-collapsible-toggle"> {' ' + getLangText('I agree to the Terms of Service the art price') + ' '} diff --git a/js/components/ascribe_forms/input_contract_agreement_checkbox.js b/js/components/ascribe_forms/input_contract_agreement_checkbox.js index 61235631..7008fe31 100644 --- a/js/components/ascribe_forms/input_contract_agreement_checkbox.js +++ b/js/components/ascribe_forms/input_contract_agreement_checkbox.js @@ -156,7 +156,7 @@ const InputContractAgreementCheckbox = React.createClass({ return (
    + style={{paddingBottom: '0.25em'}}> + onBlur={onBlur} + placeholder={placeholder} /> ); } else { - textarea =
    {this.state.value}
    ; + // Can only convert links when not editable, as textarea does not support anchors + return
    {convertLinks ? anchorize(value) : value}
    ; } - - return textarea; } }); diff --git a/js/components/ascribe_forms/property.js b/js/components/ascribe_forms/property.js index 432591bf..6d46a9d3 100644 --- a/js/components/ascribe_forms/property.js +++ b/js/components/ascribe_forms/property.js @@ -240,7 +240,17 @@ const Property = React.createClass({ }, handleCheckboxToggle() { - this.setExpanded(!this.state.expanded); + const expanded = !this.state.expanded; + + this.setExpanded(expanded); + + // Reset the value to be the initial value when the checkbox is unticked since the + // user doesn't want to specify their own value. + if (!expanded) { + this.setState({ + value: this.state.initialValue + }); + } }, renderChildren(style) { diff --git a/js/components/ascribe_modal/modal_wrapper.js b/js/components/ascribe_modal/modal_wrapper.js index fd77e5ae..511e7f8c 100644 --- a/js/components/ascribe_modal/modal_wrapper.js +++ b/js/components/ascribe_modal/modal_wrapper.js @@ -1,19 +1,20 @@ 'use strict'; import React from 'react'; -import ReactAddons from 'react/addons'; import Modal from 'react-bootstrap/lib/Modal'; let ModalWrapper = React.createClass({ propTypes: { - trigger: React.PropTypes.element, title: React.PropTypes.oneOfType([ React.PropTypes.arrayOf(React.PropTypes.element), React.PropTypes.element, React.PropTypes.string ]).isRequired, - handleSuccess: React.PropTypes.func.isRequired, + + handleCancel: React.PropTypes.func, + handleSuccess: React.PropTypes.func, + trigger: React.PropTypes.element, children: React.PropTypes.oneOfType([ React.PropTypes.arrayOf(React.PropTypes.element), React.PropTypes.element @@ -38,14 +39,25 @@ let ModalWrapper = React.createClass({ }); }, + handleCancel() { + if (typeof this.props.handleCancel === 'function') { + this.props.handleCancel(); + } + + this.hide(); + }, + handleSuccess(response) { - this.props.handleSuccess(response); + if (typeof this.props.handleSuccess === 'function') { + this.props.handleSuccess(response); + } + this.hide(); }, renderChildren() { - return ReactAddons.Children.map(this.props.children, (child) => { - return ReactAddons.addons.cloneWithProps(child, { + return React.Children.map(this.props.children, (child) => { + return React.cloneElement(child, { handleSuccess: (response) => { if (typeof child.props.handleSuccess === 'function') { child.props.handleSuccess(response); @@ -60,14 +72,23 @@ let ModalWrapper = React.createClass({ render() { const { trigger, title } = this.props; - // If the trigger component exists, we add the ModalWrapper's show() as its onClick method. + // If the trigger component exists, we add the ModalWrapper's show() to its onClick method. // The trigger component should, in most cases, be a button. - const clonedTrigger = React.isValidElement(trigger) ? React.cloneElement(trigger, {onClick: this.show}) - : null; + const clonedTrigger = React.isValidElement(trigger) ? + React.cloneElement(trigger, { + onClick: (...params) => { + if (typeof trigger.props.onClick === 'function') { + trigger.props.onClick(...params); + } + + this.show(); + } + }) : null; + return ( {clonedTrigger} - + {title} diff --git a/js/components/ascribe_piece_list_toolbar/piece_list_toolbar_filter_widget.js b/js/components/ascribe_piece_list_toolbar/piece_list_toolbar_filter_widget.js index edb29e85..c9791dbe 100644 --- a/js/components/ascribe_piece_list_toolbar/piece_list_toolbar_filter_widget.js +++ b/js/components/ascribe_piece_list_toolbar/piece_list_toolbar_filter_widget.js @@ -84,6 +84,7 @@ let PieceListToolbarFilterWidget = React.createClass({ if (this.props.filterParams && this.props.filterParams.length) { return ( diff --git a/js/components/ascribe_piece_list_toolbar/piece_list_toolbar_order_widget.js b/js/components/ascribe_piece_list_toolbar/piece_list_toolbar_order_widget.js index 5257cc07..da9bae43 100644 --- a/js/components/ascribe_piece_list_toolbar/piece_list_toolbar_order_widget.js +++ b/js/components/ascribe_piece_list_toolbar/piece_list_toolbar_order_widget.js @@ -45,7 +45,7 @@ let PieceListToolbarOrderWidget = React.createClass({ }, render() { - let filterIcon = ( + let orderIcon = ( · @@ -55,9 +55,10 @@ let PieceListToolbarOrderWidget = React.createClass({ if (this.props.orderParams && this.props.orderParams.length) { return ( + className="ascribe-piece-list-toolbar-filter-widget" + title={orderIcon}>
  • {getLangText('Sort by')}:
  • diff --git a/js/components/ascribe_settings/account_settings.js b/js/components/ascribe_settings/account_settings.js index c650358c..57d66171 100644 --- a/js/components/ascribe_settings/account_settings.js +++ b/js/components/ascribe_settings/account_settings.js @@ -35,7 +35,7 @@ let AccountSettings = React.createClass({ getFormDataProfile(){ return {'email': this.props.currentUser.email}; }, - + render() { let content = ; let profile = null; @@ -78,8 +78,7 @@ let AccountSettings = React.createClass({ getFormData={this.getFormDataProfile}> + className="ascribe-property-collapsible-toggle"> diff --git a/js/components/ascribe_settings/contract_settings.js b/js/components/ascribe_settings/contract_settings.js index e825e4b6..3b5654cb 100644 --- a/js/components/ascribe_settings/contract_settings.js +++ b/js/components/ascribe_settings/contract_settings.js @@ -18,7 +18,7 @@ import AclProxy from '../acl_proxy'; import { getLangText } from '../../utils/lang_utils'; import { setDocumentTitle } from '../../utils/dom_utils'; -import { mergeOptions, truncateTextAtCharIndex } from '../../utils/general_utils'; +import { truncateTextAtCharIndex } from '../../utils/general_utils'; let ContractSettings = React.createClass({ @@ -53,11 +53,11 @@ let ContractSettings = React.createClass({ ContractListActions.removeContract(contract.id) .then((response) => { ContractListActions.fetchContractList(true); - let notification = new GlobalNotificationModel(response.notification, 'success', 4000); + const notification = new GlobalNotificationModel(response.notification, 'success', 4000); GlobalNotificationActions.appendGlobalNotification(notification); }) .catch((err) => { - let notification = new GlobalNotificationModel(err, 'danger', 10000); + const notification = new GlobalNotificationModel(err, 'danger', 10000); GlobalNotificationActions.appendGlobalNotification(notification); }); }; @@ -82,12 +82,11 @@ let ContractSettings = React.createClass({ if (publicContracts.length === 0) { createPublicContractForm = ( + isPublic={true} /> ); } @@ -104,7 +103,7 @@ let ContractSettings = React.createClass({ {publicContracts.map((contract, i) => { return ( - +
    + fileClassToUpload={{ + singular: getLangText('new contract'), + plural: getLangText('new contracts') + }} + isPublic={false} /> {privateContracts.map((contract, i) => { return ( - + { - // Display feedback to the user - let notification = new GlobalNotificationModel(getLangText('Contract %s successfully updated', res.name), 'success', 5000); + const notification = new GlobalNotificationModel(getLangText('Contract %s successfully updated', contract.name), 'success', 5000); GlobalNotificationActions.appendGlobalNotification(notification); // and refresh the contract list to get the updated contracs - return ContractListActions.fetchContractList(true); - }) - .then(() => { - // Also, reset the fineuploader component so that the user can again 'update' his contract - this.refs.fineuploader.reset(); - }) - .catch((err) => { - console.logGlobal(err); - let notification = new GlobalNotificationModel(getLangText('Contract could not be updated'), 'success', 5000); + return ContractListActions + .fetchContractList(true) + // Also, reset the fineuploader component if fetch is successful so that the user can again 'update' his contract + .then(this.refs.fineuploader.reset) + .catch((err) => { + const notification = new GlobalNotificationModel(getLangText('Latest contract failed to load'), 'danger', 5000); + GlobalNotificationActions.appendGlobalNotification(notification); + + return Promise.reject(err); + }); + }, (err) => { + const notification = new GlobalNotificationModel(getLangText('Contract could not be updated'), 'danger', 5000); GlobalNotificationActions.appendGlobalNotification(notification); - }); + + return Promise.reject(err); + }) + .catch(console.logGlobal); }, render() { return ( {/* So that ReactS3FineUploader is not complaining */}} - signature={{ - endpoint: AppConstants.serverUrl + 's3/signature/', - customHeaders: { - 'X-CSRFToken': getCookie(AppConstants.csrftoken) - } - }} - deleteFile={{ - enabled: true, - method: 'DELETE', - endpoint: AppConstants.serverUrl + 's3/delete', - customHeaders: { - 'X-CSRFToken': getCookie(AppConstants.csrftoken) - } - }} - fileClassToUpload={{ - singular: getLangText('UPDATE'), - plural: getLangText('UPDATE') - }} - isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile} - submitFile={this.submitFile} /> + ref='fineuploader' + fileInputElement={UploadButton({ showLabel: false })} + keyRoutine={{ + url: AppConstants.serverUrl + 's3/key/', + fileClass: 'contract' + }} + createBlobRoutine={{ + url: ApiUrls.blob_contracts + }} + validation={{ + itemLimit: validationTypes.registerWork.itemLimit, + sizeLimit: validationTypes.additionalData.sizeLimit, + allowedExtensions: ['pdf'] + }} + setIsUploadReady={() =>{/* So that ReactS3FineUploader is not complaining */}} + signature={{ + endpoint: AppConstants.serverUrl + 's3/signature/', + customHeaders: { + 'X-CSRFToken': getCookie(AppConstants.csrftoken) + } + }} + deleteFile={{ + enabled: true, + method: 'DELETE', + endpoint: AppConstants.serverUrl + 's3/delete', + customHeaders: { + 'X-CSRFToken': getCookie(AppConstants.csrftoken) + } + }} + fileClassToUpload={{ + singular: getLangText('UPDATE'), + plural: getLangText('UPDATE') + }} + isReadyForFormSubmission={formSubmissionValidation.atLeastOneUploadedFile} + submitFile={this.submitFile} /> ); } }); diff --git a/js/components/ascribe_settings/webhook_settings.js b/js/components/ascribe_settings/webhook_settings.js index 9deecbcd..4928c408 100644 --- a/js/components/ascribe_settings/webhook_settings.js +++ b/js/components/ascribe_settings/webhook_settings.js @@ -34,7 +34,6 @@ let WebhookSettings = React.createClass({ componentDidMount() { WebhookStore.listen(this.onChange); WebhookActions.fetchWebhooks(); - WebhookActions.fetchWebhookEvents(); }, componentWillUnmount() { @@ -49,7 +48,7 @@ let WebhookSettings = React.createClass({ return (event) => { WebhookActions.removeWebhook(webhookId); - let notification = new GlobalNotificationModel(getLangText('Webhook deleted'), 'success', 2000); + const notification = new GlobalNotificationModel(getLangText('Webhook deleted'), 'success', 2000); GlobalNotificationActions.appendGlobalNotification(notification); }; }, @@ -57,16 +56,16 @@ let WebhookSettings = React.createClass({ handleCreateSuccess() { this.refs.webhookCreateForm.reset(); WebhookActions.fetchWebhooks(true); - let notification = new GlobalNotificationModel(getLangText('Webhook successfully created'), 'success', 5000); + + const notification = new GlobalNotificationModel(getLangText('Webhook successfully created'), 'success', 5000); GlobalNotificationActions.appendGlobalNotification(notification); }, - getWebhooks(){ - let content = ; - + getWebhooks() { if (this.state.webhooks) { - content = this.state.webhooks.map(function(webhook, i) { + return this.state.webhooks.map(function(webhook, i) { const event = webhook.event.split('.')[0]; + return (
    - }/> + } /> ); }, this); + } else { + return ( + + ); } - return content; }, getEvents() { @@ -110,18 +112,18 @@ let WebhookSettings = React.createClass({ ); })}
    ); + } else { + return null; } - return null; }, - render() { return (
    - + - { this.getEvents() } + {this.getEvents()} + required />
    @@ -162,4 +163,4 @@ let WebhookSettings = React.createClass({ } }); -export default WebhookSettings; \ No newline at end of file +export default WebhookSettings; diff --git a/js/components/ascribe_social_share/facebook_share_button.js b/js/components/ascribe_social_share/facebook_share_button.js index aa0b6691..d5fbf699 100644 --- a/js/components/ascribe_social_share/facebook_share_button.js +++ b/js/components/ascribe_social_share/facebook_share_button.js @@ -2,6 +2,8 @@ import React from 'react'; +import FacebookHandler from '../../third_party/facebook_handler'; + import AppConstants from '../../constants/application_constants'; import { InjectInHeadUtils } from '../../utils/inject_utils'; @@ -17,24 +19,40 @@ let FacebookShareButton = React.createClass({ }; }, - componentDidMount() { - /** - * Ideally we would only use FB.XFBML.parse() on the component that we're - * mounting, but doing this when we first load the FB sdk causes unpredictable behaviour. - * The button sometimes doesn't get initialized, likely because FB hasn't properly - * been initialized yet. - * - * To circumvent this, we always have the sdk parse the entire DOM on the initial load - * (see FacebookHandler) and then use FB.XFBML.parse() on the mounting component later. - */ - - InjectInHeadUtils - .inject(AppConstants.facebook.sdkUrl) - .then(() => { FB.XFBML.parse(this.refs.fbShareButton.getDOMNode().parentElement) }); + getInitialState() { + return FacebookHandler.getState(); }, - shouldComponentUpdate(nextProps) { - return this.props.type !== nextProps.type; + componentDidMount() { + FacebookHandler.listen(this.onChange); + + this.loadFacebook(); + }, + + shouldComponentUpdate(nextProps, nextState) { + // Don't update if the props haven't changed or the FB SDK loading status is still the same + return this.props.type !== nextProps.type || nextState.loaded !== this.state.loaded; + }, + + componentDidUpdate() { + // If the component changes, we need to reparse the share button's XFBML. + // To prevent cases where the Facebook SDK hasn't been loaded yet at this stage, + // let's make sure that it's injected before trying to reparse. + this.loadFacebook(); + }, + + onChange(state) { + this.setState(state); + }, + + loadFacebook() { + InjectInHeadUtils + .inject(AppConstants.facebook.sdkUrl) + .then(() => { + if (this.state.loaded) { + FB.XFBML.parse(this.refs.fbShareButton.getDOMNode().parent) + } + }); }, render() { diff --git a/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_preview.js b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_preview.js index ca1be2d2..0f95427d 100644 --- a/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_preview.js +++ b/js/components/ascribe_uploader/ascribe_file_drag_and_drop/file_drag_and_drop_preview.js @@ -23,6 +23,7 @@ const FileDragAndDropPreview = React.createClass({ s3Url: string, s3UrlSafe: string }).isRequired, + handleDeleteFile: func, handleCancelFile: func, handlePauseFile: func, @@ -33,9 +34,9 @@ const FileDragAndDropPreview = React.createClass({ }, toggleUploadProcess() { - if(this.props.file.status === 'uploading') { + if (this.props.file.status === 'uploading') { this.props.handlePauseFile(this.props.file.id); - } else if(this.props.file.status === 'paused') { + } else if (this.props.file.status === 'paused') { this.props.handleResumeFile(this.props.file.id); } }, @@ -54,13 +55,13 @@ const FileDragAndDropPreview = React.createClass({ (file.status === 'upload successful' || file.status === 'online') && file.s3UrlSafe) { handleDeleteFile(file.id); - } else if(handleCancelFile) { + } else if (handleCancelFile) { handleCancelFile(file.id); } }, handleDownloadFile() { - if(this.props.file.s3Url) { + if (this.props.file.s3Url) { // This simply opens a new browser tab with the url provided open(this.props.file.s3Url); } @@ -69,7 +70,7 @@ const FileDragAndDropPreview = React.createClass({ getFileName() { const { numberOfDisplayedFiles, file } = this.props; - if(numberOfDisplayedFiles === 1) { + if (numberOfDisplayedFiles === 1) { return ( {truncateTextAtCharIndex(file.name, 30, '(...).' + extractFileExtensionFromString(file.name))} @@ -81,7 +82,7 @@ const FileDragAndDropPreview = React.createClass({ }, getRemoveButton() { - if(this.props.areAssetsEditable) { + if (this.props.areAssetsEditable) { return (
    @@ -66,22 +66,19 @@ const FileDragAndDropPreviewOther = React.createClass({ ); } else { actionSymbol = ( - + ); } return ( -
    +
    -
    -
    - {actionSymbol} -

    {'.' + type}

    -
    + className="ascribe-progress-bar ascribe-progress-bar-xs" /> +
    + {actionSymbol} +

    {'.' + (type ? type : 'file')}

    ); diff --git a/js/components/ascribe_uploader/ascribe_upload_button/upload_button.js b/js/components/ascribe_uploader/ascribe_upload_button/upload_button.js index 6612f968..ffd26a13 100644 --- a/js/components/ascribe_uploader/ascribe_upload_button/upload_button.js +++ b/js/components/ascribe_uploader/ascribe_upload_button/upload_button.js @@ -1,6 +1,7 @@ 'use strict'; import React from 'react'; +import classNames from 'classnames'; import { displayValidProgressFilesFilter } from '../react_s3_fine_uploader_utils'; import { getLangText } from '../../../utils/lang_utils'; @@ -9,7 +10,7 @@ import { truncateTextAtCharIndex } from '../../../utils/general_utils'; const { func, array, bool, shape, string } = React.PropTypes; -export default function UploadButton({ className = 'btn btn-default btn-sm' } = {}) { +export default function UploadButton({ className = 'btn btn-default btn-sm', showLabel = true } = {}) { return React.createClass({ displayName: 'UploadButton', @@ -119,28 +120,28 @@ export default function UploadButton({ className = 'btn btn-default btn-sm' } = }, getUploadedFileLabel() { - const uploadedFile = this.getUploadedFile(); - const uploadingFiles = this.getUploadingFiles(); + if (showLabel) { + const uploadedFile = this.getUploadedFile(); + const uploadingFiles = this.getUploadingFiles(); - if(uploadingFiles.length) { - return ( - - {' ' + truncateTextAtCharIndex(uploadingFiles[0].name, 40) + ' '} - [
    {getLangText('cancel upload')}] - - ); - } else if(uploadedFile) { - return ( - - - {' ' + truncateTextAtCharIndex(uploadedFile.name, 40) + ' '} - [{getLangText('remove')}] - - ); - } else { - return ( - {getLangText('No file chosen')} - ); + if (uploadingFiles.length) { + return ( + + {' ' + truncateTextAtCharIndex(uploadingFiles[0].name, 40) + ' '} + [{getLangText('cancel upload')}] + + ); + } else if (uploadedFile) { + return ( + + + {' ' + truncateTextAtCharIndex(uploadedFile.name, 40) + ' '} + [{getLangText('remove')}] + + ); + } else { + return {getLangText('No file chosen')}; + } } }, @@ -158,7 +159,7 @@ export default function UploadButton({ className = 'btn btn-default btn-sm' } = * Therefore the wrapping component needs to be an `anchor` tag instead of a `button` */ return ( -
    +
    {/* The button needs to be of `type="button"` as it would otherwise submit the form its in. diff --git a/js/components/ascribe_uploader/react_s3_fine_uploader.js b/js/components/ascribe_uploader/react_s3_fine_uploader.js index 877146a5..447b76ea 100644 --- a/js/components/ascribe_uploader/react_s3_fine_uploader.js +++ b/js/components/ascribe_uploader/react_s3_fine_uploader.js @@ -13,9 +13,9 @@ import GlobalNotificationActions from '../../actions/global_notification_actions import AppConstants from '../../constants/application_constants'; -import { computeHashOfFile } from '../../utils/file_utils'; import { displayValidFilesFilter, transformAllowedExtensionsToInputAcceptProp } from './react_s3_fine_uploader_utils'; import { getCookie } from '../../utils/fetch_api_utils'; +import { computeHashOfFile, extractFileExtensionFromString } from '../../utils/file_utils'; import { getLangText } from '../../utils/lang_utils'; @@ -36,17 +36,11 @@ const ReactS3FineUploader = React.createClass({ keyRoutine: shape({ url: string, fileClass: string, - pieceId: oneOfType([ - string, - number - ]) + pieceId: number }), createBlobRoutine: shape({ url: string, - pieceId: oneOfType([ - string, - number - ]) + pieceId: number }), handleChangedFile: func, // is for when a file is dropped or selected submitFile: func, // is for when a file has been successfully uploaded, TODO: rename to handleSubmitFile @@ -97,7 +91,7 @@ const ReactS3FineUploader = React.createClass({ }), validation: shape({ itemLimit: number, - sizeLimit: string, + sizeLimit: number, allowedExtensions: arrayOf(string) }), messages: shape({ @@ -284,22 +278,6 @@ const ReactS3FineUploader = React.createClass({ this.setState(this.getInitialState()); }, - // Cancel uploads and clear previously selected files on the input element - cancelUploads(id) { - typeof id !== 'undefined' ? this.state.uploader.cancel(id) : this.state.uploader.cancelAll(); - - // Reset the file input element to clear the previously selected files so that - // the user can reselect them again. - this.clearFileSelection(); - }, - - clearFileSelection() { - const { fileInput } = this.refs; - if (fileInput && typeof fileInput.clearSelection === 'function') { - fileInput.clearSelection(); - } - }, - requestKey(fileId) { let filename = this.state.uploader.getName(fileId); let uuid = this.state.uploader.getUuid(fileId); @@ -390,6 +368,107 @@ const ReactS3FineUploader = React.createClass({ }); }, + // Cancel uploads and clear previously selected files on the input element + cancelUploads(id) { + typeof id !== 'undefined' ? this.state.uploader.cancel(id) : this.state.uploader.cancelAll(); + + // Reset the file input element to clear the previously selected files so that + // the user can reselect them again. + this.clearFileSelection(); + }, + + clearFileSelection() { + const { fileInput } = this.refs; + if (fileInput && typeof fileInput.clearSelection === 'function') { + fileInput.clearSelection(); + } + }, + + getAllowedExtensions() { + const { validation: { allowedExtensions } = {} } = this.props; + + if (allowedExtensions && allowedExtensions.length) { + return transformAllowedExtensionsToInputAcceptProp(allowedExtensions); + } else { + return null; + } + }, + + getXhrErrorComment(xhr) { + if (xhr) { + return { + response: xhr.response, + url: xhr.responseURL, + status: xhr.status, + statusText: xhr.statusText + }; + } + }, + + isDropzoneInactive() { + const filesToDisplay = this.state.filesToUpload.filter((file) => file.status !== 'deleted' && file.status !== 'canceled' && file.size !== -1); + + if ((this.props.enableLocalHashing && !this.props.uploadMethod) || !this.props.areAssetsEditable || !this.props.multiple && filesToDisplay.length > 0) { + return true; + } else { + return false; + } + }, + + isFileValid(file) { + const { validation: { allowedExtensions, sizeLimit = 0 }, onValidationFailed } = this.props; + const fileExt = extractFileExtensionFromString(file.name); + + if (file.size > sizeLimit) { + const fileSizeInMegaBytes = sizeLimit / 1000000; + + const notification = new GlobalNotificationModel(getLangText('A file you submitted is bigger than ' + fileSizeInMegaBytes + 'MB.'), 'danger', 5000); + GlobalNotificationActions.appendGlobalNotification(notification); + + if (typeof onValidationFailed === 'function') { + onValidationFailed(file); + } + + return false; + } else if (allowedExtensions && !allowedExtensions.includes(fileExt)) { + const notification = new GlobalNotificationModel(getLangText(`The file you've submitted is of an invalid file format: Valid format(s): ${allowedExtensions.join(', ')}`), 'danger', 5000); + GlobalNotificationActions.appendGlobalNotification(notification); + + return false; + } else { + return true; + } + }, + + selectValidFiles(files) { + return Array.from(files).reduce((validFiles, file) => { + if (this.isFileValid(file)) { + validFiles.push(file); + } + return validFiles; + }, []); + }, + + // This method has been made promise-based to immediately afterwards + // call a callback function (instantly after this.setState went through) + // This is e.g. needed when showing/hiding the optional thumbnail upload + // field in the registration form + setStatusOfFile(fileId, status) { + return Q.Promise((resolve) => { + let changeSet = {}; + + if(status === 'deleted' || status === 'canceled') { + changeSet.progress = { $set: 0 }; + } + + changeSet.status = { $set: status }; + + let filesToUpload = React.addons.update(this.state.filesToUpload, { [fileId]: changeSet }); + + this.setState({ filesToUpload }, resolve); + }); + }, + setThumbnailForFileId(fileId, url) { const { filesToUpload } = this.state; @@ -512,34 +591,6 @@ const ReactS3FineUploader = React.createClass({ GlobalNotificationActions.appendGlobalNotification(notification); }, - getXhrErrorComment(xhr) { - if (xhr) { - return { - response: xhr.response, - url: xhr.responseURL, - status: xhr.status, - statusText: xhr.statusText - }; - } - }, - - isFileValid(file) { - if (file.size > this.props.validation.sizeLimit) { - const fileSizeInMegaBytes = this.props.validation.sizeLimit / 1000000; - - const notification = new GlobalNotificationModel(getLangText('A file you submitted is bigger than ' + fileSizeInMegaBytes + 'MB.'), 'danger', 5000); - GlobalNotificationActions.appendGlobalNotification(notification); - - if (typeof this.props.onValidationFailed === 'function') { - this.props.onValidationFailed(file); - } - - return false; - } else { - return true; - } - }, - onCancel(id) { // when a upload is canceled, we need to update this components file array this.setStatusOfFile(id, 'canceled') @@ -676,6 +727,13 @@ const ReactS3FineUploader = React.createClass({ this.cancelUploads(fileId); }, + handleCancelHashing() { + // Every progress tick of the hashing function in handleUploadFile there is a + // check if this.state.hashingProgress is -1. If so, there is an error thrown that cancels + // the hashing of all files immediately. + this.setState({ hashingProgress: -1 }); + }, + handlePauseFile(fileId) { if(this.state.uploader.pauseUpload(fileId)) { this.setStatusOfFile(fileId, 'paused'); @@ -704,15 +762,8 @@ const ReactS3FineUploader = React.createClass({ return; } - // validate each submitted file if it fits the file size - let validFiles = []; - for(let i = 0; i < files.length; i++) { - if(this.isFileValid(files[i])) { - validFiles.push(files[i]); - } - } - // override standard files list with only valid files - files = validFiles; + // Select only the submitted files that fit the file size and allowed extensions + files = this.selectValidFiles(files); // if multiple is set to false and user drops multiple files into the dropzone, // take the first one and notify user that only one file can be submitted @@ -823,13 +874,6 @@ const ReactS3FineUploader = React.createClass({ } }, - handleCancelHashing() { - // Every progress tick of the hashing function in handleUploadFile there is a - // check if this.state.hashingProgress is -1. If so, there is an error thrown that cancels - // the hashing of all files immediately. - this.setState({ hashingProgress: -1 }); - }, - // ReactFineUploader is essentially just a react layer around s3 fineuploader. // However, since we need to display the status of a file (progress, uploading) as well as // be able to execute actions on a currently uploading file we need to exactly sync the file list @@ -899,46 +943,6 @@ const ReactS3FineUploader = React.createClass({ }); }, - // This method has been made promise-based to immediately afterwards - // call a callback function (instantly after this.setState went through) - // This is e.g. needed when showing/hiding the optional thumbnail upload - // field in the registration form - setStatusOfFile(fileId, status) { - return Q.Promise((resolve) => { - let changeSet = {}; - - if(status === 'deleted' || status === 'canceled') { - changeSet.progress = { $set: 0 }; - } - - changeSet.status = { $set: status }; - - let filesToUpload = React.addons.update(this.state.filesToUpload, { [fileId]: changeSet }); - - this.setState({ filesToUpload }, resolve); - }); - }, - - isDropzoneInactive() { - const filesToDisplay = this.state.filesToUpload.filter((file) => file.status !== 'deleted' && file.status !== 'canceled' && file.size !== -1); - - if ((this.props.enableLocalHashing && !this.props.uploadMethod) || !this.props.areAssetsEditable || !this.props.multiple && filesToDisplay.length > 0) { - return true; - } else { - return false; - } - }, - - getAllowedExtensions() { - let { validation } = this.props; - - if(validation && validation.allowedExtensions && validation.allowedExtensions.length > 0) { - return transformAllowedExtensionsToInputAcceptProp(validation.allowedExtensions); - } else { - return null; - } - }, - render() { const { multiple, diff --git a/js/components/coa_verify_container.js b/js/components/coa_verify_container.js index a3ac5037..f0649196 100644 --- a/js/components/coa_verify_container.js +++ b/js/components/coa_verify_container.js @@ -32,7 +32,7 @@ let CoaVerifyContainer = React.createClass({ return (
    -
    +
    {getLangText('Verify your Certificate of Authenticity')}
    @@ -42,7 +42,7 @@ let CoaVerifyContainer = React.createClass({ signature={signature}/>

    - {getLangText('ascribe is using the following public key for verification')}: + {getLangText('ascribe is using the following public key for verification')}:
                     -----BEGIN PUBLIC KEY-----
    @@ -75,46 +75,44 @@ let CoaVerifyForm = React.createClass({
             const { message, signature } = this.props;
     
             return (
    -            
    -
    - {getLangText('Verify your Certificate of Authenticity')} - } - spinner={ - - - - }> - - - - - - -
    -
    -
    +
    + {getLangText('Verify your Certificate of Authenticity')} + + } + spinner={ + + + + }> + + + + + + +
    +
    ); } }); diff --git a/js/components/error_not_found_page.js b/js/components/error_not_found_page.js index a0e4cbb5..046c07a0 100644 --- a/js/components/error_not_found_page.js +++ b/js/components/error_not_found_page.js @@ -1,6 +1,7 @@ 'use strict'; import React from 'react'; +import { History } from 'react-router'; import { getLangText } from '../utils/lang_utils'; @@ -17,12 +18,25 @@ let ErrorNotFoundPage = React.createClass({ location: React.PropTypes.object }, + mixins: [History], + getDefaultProps() { return { message: getLangText("Oops, the page you are looking for doesn't exist.") }; }, + componentDidMount() { + // The previous page, if any, is the second item in the locationQueue + const { locationQueue: [ , previousPage ] } = this.history; + + if (previousPage) { + console.logGlobal('Page not found', { + previousPath: previousPage.pathname + }); + } + }, + render() { return (
    diff --git a/js/components/footer.js b/js/components/footer.js index f2e35dfc..3010da4d 100644 --- a/js/components/footer.js +++ b/js/components/footer.js @@ -5,8 +5,12 @@ import React from 'react'; import { getLangText } from '../utils/lang_utils'; let Footer = React.createClass({ + propTypes: { + activeRoute: React.PropTypes.object.isRequired + }, + render() { - return ( + return !this.props.activeRoute.hideFooter ? (


    @@ -24,7 +28,7 @@ let Footer = React.createClass({

    - ); + ) : null; } }); diff --git a/js/components/global_action.js b/js/components/global_action.js deleted file mode 100644 index 80df0c75..00000000 --- a/js/components/global_action.js +++ /dev/null @@ -1,43 +0,0 @@ -'use strict'; - -import React from 'react'; - -let GlobalAction = React.createClass({ - propTypes: { - requestActions: React.PropTypes.object - }, - - render() { - let pieceActions = null; - if (this.props.requestActions && this.props.requestActions.pieces){ - pieceActions = this.props.requestActions.pieces.map((item) => { - return ( -
    - {item} -
    ); - }); - } - let editionActions = null; - if (this.props.requestActions && this.props.requestActions.editions){ - editionActions = Object.keys(this.props.requestActions.editions).map((pieceId) => { - return this.props.requestActions.editions[pieceId].map((item) => { - return ( -
    - {item} -
    ); - }); - }); - } - - if (pieceActions || editionActions) { - return ( -
    - {pieceActions} - {editionActions} -
    ); - } - return null; - } -}); - -export default GlobalAction; \ No newline at end of file diff --git a/js/components/header.js b/js/components/header.js index fcc19e31..ce1cc4d8 100644 --- a/js/components/header.js +++ b/js/components/header.js @@ -16,6 +16,8 @@ import LinkContainer from 'react-router-bootstrap/lib/LinkContainer'; import EventActions from '../actions/event_actions'; +import PieceListStore from '../stores/piece_list_store'; + import AclProxy from './acl_proxy'; import HeaderNotifications from './header_notification'; import HeaderNotificationDebug from './header_notification_debug'; @@ -27,12 +29,21 @@ import { constructHead } from '../utils/dom_utils'; let Header = React.createClass({ propTypes: { + // Provided from AscribeApp currentUser: React.PropTypes.object.isRequired, routes: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, whitelabel: React.PropTypes.object.isRequired }, + getInitialState() { + return PieceListStore.getState(); + }, + componentDidMount() { + // Listen to the piece list store, but don't fetch immediately to avoid + // conflicts with routes that may need to wait to load the piece list + PieceListStore.listen(this.onChange); + // react-bootstrap 0.25.1 has a bug in which it doesn't // close the mobile expanded navigation after a click by itself. // To get rid of this, we set the state of the component ourselves. @@ -40,9 +51,14 @@ let Header = React.createClass({ }, componentWillUnmount() { + PieceListStore.unlisten(this.onChange); //history.unlisten(this.onRouteChange); }, + onChange(state) { + this.setState(state); + }, + getLogo() { const { whitelabel } = this.props; @@ -115,7 +131,9 @@ let Header = React.createClass({ }, render() { - const { currentUser, routes } = this.props; + const { currentUser, routes } = this.props; + const { unfilteredPieceListCount } = this.state; + let account; let signup; let navRoutesLinks; @@ -124,6 +142,7 @@ let Header = React.createClass({ account = ( ); + // Let's assume that if the piece list hasn't loaded yet (ie. when unfilteredPieceListCount === -1) + // then the user has pieces + // FIXME: this doesn't work that well as the user may not load their piece list + // until much later, so we would show the 'Collection' header as available until + // they actually click on it and get redirected to piece registration. navRoutesLinks = ( + right + hasPieces={!!unfilteredPieceListCount} + routes={routes} + userAcl={currentUser.acl} /> ); } else { account = ( diff --git a/js/components/header_notification.js b/js/components/header_notification.js index 4b375685..784f8786 100644 --- a/js/components/header_notification.js +++ b/js/components/header_notification.js @@ -126,6 +126,7 @@ let HeaderNotifications = React.createClass({
    ); + } else { + return null; } - return null; } }); let PrizePieceRatings = React.createClass({ propTypes: { - loadPiece: React.PropTypes.func, - piece: React.PropTypes.object, - currentUser: React.PropTypes.object, + currentUser: React.PropTypes.object.isRequired, + loadPiece: React.PropTypes.func.isRequired, + piece: React.PropTypes.object.isRequired, + selectedPrizeActionButton: React.PropTypes.func }, @@ -243,12 +239,18 @@ let PrizePieceRatings = React.createClass({ }, componentDidMount() { - PieceListStore.listen(this.onChange); - PrizeStore.listen(this.onChange); PrizeRatingStore.listen(this.onChange); + PrizeStore.listen(this.onChange); + PieceListStore.listen(this.onChange); PrizeActions.fetchPrize(); - this.fetchPrizeRatings(); + this.fetchRatingsIfAuthorized(); + }, + + componentWillReceiveProps(nextProps) { + if (nextProps.currentUser.email !== this.props.currentUser.email) { + this.fetchRatingsIfAuthorized(); + } }, componentWillUnmount() { @@ -263,7 +265,7 @@ let PrizePieceRatings = React.createClass({ // with the problem. onChange(state) { if (state.prize && state.prize.active_round != this.state.prize.active_round) { - this.fetchPrizeRatings(state); + this.fetchRatingsIfAuthorized(state); } this.setState(state); @@ -278,9 +280,19 @@ let PrizePieceRatings = React.createClass({ } }, - fetchPrizeRatings(state = this.state) { - PrizeRatingActions.fetchOne(this.props.piece.id, state.prize.active_round); - PrizeRatingActions.fetchAverage(this.props.piece.id, state.prize.active_round); + fetchRatingsIfAuthorized(state = this.state) { + const { + currentUser: { + is_admin: isAdmin, + is_judge: isJudge, + is_jury: isJury + }, + piece: { id: pieceId } } = this.props; + + if (state.prize && 'active_round' in state.prize && (isAdmin || isJudge || isJury)) { + PrizeRatingActions.fetchOne(pieceId, state.prize.active_round); + PrizeRatingActions.fetchAverage(pieceId, state.prize.active_round); + } }, onRatingClick(event, args) { @@ -305,9 +317,10 @@ let PrizePieceRatings = React.createClass({ }, refreshPieceData() { + const { filterBy, orderAsc, orderBy, page, pageSize, search } = this.state; + this.props.loadPiece(); - PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, - this.state.orderBy, this.state.orderAsc, this.state.filterBy); + PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy }); }, onSelectChange() { @@ -324,7 +337,7 @@ let PrizePieceRatings = React.createClass({ }, render() { - if (this.props.piece && this.props.currentUser && this.props.currentUser.is_judge && this.state.average) { + if (this.props.piece.id && this.props.currentUser.is_judge && this.state.average) { // Judge sees shortlisting, average and per-jury notes return (
    @@ -356,7 +369,7 @@ let PrizePieceRatings = React.createClass({ size='md' step={0.5} rating={this.state.average} - ratingAmount={5}/> + ratingAmount={5} />

    {this.state.ratings.map((item, i) => { @@ -381,7 +394,7 @@ let PrizePieceRatings = React.createClass({ size='sm' step={0.5} rating={item.rating} - ratingAmount={5}/> + ratingAmount={5} /> {item.user} {note} @@ -392,7 +405,7 @@ let PrizePieceRatings = React.createClass({
    ); - } else if (this.props.currentUser && this.props.currentUser.is_jury) { + } else if (this.props.currentUser.is_jury) { // Jury can set rating and note return (
    {return {'piece_id': this.props.piece.id}; }} + id={() => ({ 'piece_id': this.props.piece.id })} label={getLangText('Jury note')} - defaultValue={this.props.piece && this.props.piece.note_from_user ? this.props.piece.note_from_user.note : null} + defaultValue={this.props.piece.note_from_user || null} placeholder={getLangText('Enter your comments ...')} editable={true} successMessage={getLangText('Jury note saved')} url={ApiUrls.notes} - currentUser={this.props.currentUser}/> + currentUser={this.props.currentUser} /> ); } else { return null; @@ -428,39 +441,40 @@ let PrizePieceRatings = React.createClass({ let PrizePieceDetails = React.createClass({ propTypes: { - piece: React.PropTypes.object + piece: React.PropTypes.object.isRequired }, render() { const { piece } = this.props; - if (piece && - piece.prize && - piece.prize.name && - Object.keys(piece.extra_data).length !== 0) { + if (piece.prize && piece.prize.name && Object.keys(piece.extra_data).length) { return ( - - {Object.keys(piece.extra_data).sort().map((data) => { - // Remove leading number (for sorting), if any, and underscores with spaces - let label = data.replace(/^\d-/, '').replace(/_/g, ' '); - const value = piece.extra_data[data] || 'N/A'; + + {Object + .keys(piece.extra_data) + .sort() + .map((data) => { + // Remove leading number (for sorting), if any, and underscores with spaces + const label = data.replace(/^\d-/, '').replace(/_/g, ' '); + const value = piece.extra_data[data] || 'N/A'; - return ( - - - - ); - })} + return ( + + + + ); + }) + } {}} setIsUploadReady={() => {}} @@ -473,8 +487,9 @@ let PrizePieceDetails = React.createClass({ ); + } else { + return null; } - return null; } }); diff --git a/js/components/whitelabel/prize/simple_prize/components/prize_piece_list.js b/js/components/whitelabel/prize/simple_prize/components/prize_piece_list.js index 207fac30..1e2c7c9c 100644 --- a/js/components/whitelabel/prize/simple_prize/components/prize_piece_list.js +++ b/js/components/whitelabel/prize/simple_prize/components/prize_piece_list.js @@ -75,12 +75,11 @@ let PrizePieceList = React.createClass({ + orderParams={orderParams} + orderBy={currentUser.is_jury ? 'rating' : null} /> ); } }); diff --git a/js/components/whitelabel/prize/simple_prize/components/prize_register_piece.js b/js/components/whitelabel/prize/simple_prize/components/prize_register_piece.js index fcb304ca..12e6af27 100644 --- a/js/components/whitelabel/prize/simple_prize/components/prize_register_piece.js +++ b/js/components/whitelabel/prize/simple_prize/components/prize_register_piece.js @@ -75,8 +75,7 @@ let PrizeRegisterPiece = React.createClass({ + className="ascribe-property-collapsible-toggle"> {' ' + getLangText('I agree to the Terms of Service the art price') + ' '} diff --git a/js/components/whitelabel/prize/simple_prize/prize_app.js b/js/components/whitelabel/prize/simple_prize/prize_app.js index 9ca99ca3..7a7c5cbb 100644 --- a/js/components/whitelabel/prize/simple_prize/prize_app.js +++ b/js/components/whitelabel/prize/simple_prize/prize_app.js @@ -1,66 +1,34 @@ 'use strict'; import React from 'react'; - -import UserStore from '../../../../stores/user_store'; -import UserActions from '../../../../actions/user_actions'; - -import WhitelabelActions from '../../../../actions/whitelabel_actions'; -import WhitelabelStore from '../../../../stores/whitelabel_store'; +import classNames from 'classnames'; import Hero from './components/prize_hero'; +import AppBase from '../../../app_base'; import AppRouteWrapper from '../../../app_route_wrapper'; -import Header from '../../../header'; import Footer from '../../../footer'; -import GlobalNotification from '../../../global_notification'; +import Header from '../../../header'; -import { getSubdomain, mergeOptions } from '../../../../utils/general_utils'; +import { getSubdomain } from '../../../../utils/general_utils'; let PrizeApp = React.createClass({ propTypes: { + activeRoute: React.PropTypes.object.isRequired, + children: React.PropTypes.element.isRequired, history: React.PropTypes.object.isRequired, routes: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, - children: React.PropTypes.oneOfType([ - React.PropTypes.arrayOf(React.PropTypes.element), - React.PropTypes.element - ]) - }, - - getInitialState() { - return mergeOptions( - UserStore.getState(), - WhitelabelStore.getState() - ); - }, - - componentDidMount() { - UserStore.listen(this.onChange); - WhitelabelStore.listen(this.onChange); - - UserActions.fetchCurrentUser(); - WhitelabelActions.fetchWhitelabel(); - }, - - componentWillUnmount() { - UserStore.unlisten(this.onChange); - WhitelabelActions.unlisten(this.onChange); - }, - - onChange(state) { - this.setState(state); + // Provided from AppBase + currentUser: React.PropTypes.object, + whitelabel: React.PropTypes.object }, render() { - const { children, history, routes } = this.props; - const { currentUser, whitelabel } = this.state; + const { activeRoute, children, currentUser, history, routes, whitelabel } = this.props; const subdomain = getSubdomain(); - - // The second element of routes is always the active component object, where we can - // extract the path. - let path = routes[1] ? routes[1].path : null; + const path = activeRoute && activeRoute.path; let header = null; // if the path of the current activeRoute is not defined, then this is the IndexRoute @@ -76,7 +44,7 @@ let PrizeApp = React.createClass({ } return ( -
    +
    {header} -
    - - +
    ); } }); -export default PrizeApp; +export default AppBase(PrizeApp); diff --git a/js/components/whitelabel/prize/simple_prize/stores/prize_rating_store.js b/js/components/whitelabel/prize/simple_prize/stores/prize_rating_store.js index 6c478f3c..d3544f71 100644 --- a/js/components/whitelabel/prize/simple_prize/stores/prize_rating_store.js +++ b/js/components/whitelabel/prize/simple_prize/stores/prize_rating_store.js @@ -14,6 +14,18 @@ class PrizeRatingStore { }); } + getInitialState() { + this.ratings = []; + this.currentRating = null; + this.average = null; + + return { + ratings: this.ratings, + currentRating: this.currentRating, + average: this.average + }; + } + onUpdatePrizeRatings(ratings) { this.ratings = ratings; } @@ -30,18 +42,6 @@ class PrizeRatingStore { onResetPrizeRatings() { this.getInitialState(); } - - getInitialState() { - this.ratings = []; - this.currentRating = null; - this.average = null; - - return { - ratings: this.ratings, - currentRating: this.currentRating, - average: this.average - }; - } } -export default alt.createStore(PrizeRatingStore, 'PrizeRatingStore'); \ No newline at end of file +export default alt.createStore(PrizeRatingStore, 'PrizeRatingStore'); diff --git a/js/components/whitelabel/prize/sluice/components/sluice_buttons/sluice_selected_prize_action_button.js b/js/components/whitelabel/prize/sluice/components/sluice_buttons/sluice_selected_prize_action_button.js index 78056a1b..41c979d9 100644 --- a/js/components/whitelabel/prize/sluice/components/sluice_buttons/sluice_selected_prize_action_button.js +++ b/js/components/whitelabel/prize/sluice/components/sluice_buttons/sluice_selected_prize_action_button.js @@ -1,4 +1,4 @@ -'use strict' +'use strict'; import React from 'react'; import Moment from 'moment'; diff --git a/js/components/whitelabel/wallet/components/23vivi/23vivi_accordion_list/23vivi_accordion_list_item_thumbnail_placeholder.js b/js/components/whitelabel/wallet/components/23vivi/23vivi_accordion_list/23vivi_accordion_list_item_thumbnail_placeholder.js index f360c932..3a9b3943 100644 --- a/js/components/whitelabel/wallet/components/23vivi/23vivi_accordion_list/23vivi_accordion_list_item_thumbnail_placeholder.js +++ b/js/components/whitelabel/wallet/components/23vivi/23vivi_accordion_list/23vivi_accordion_list_item_thumbnail_placeholder.js @@ -1,4 +1,4 @@ -'use strict' +'use strict'; import React from 'react'; diff --git a/js/components/whitelabel/wallet/components/23vivi/23vivi_landing.js b/js/components/whitelabel/wallet/components/23vivi/23vivi_landing.js index 386f2145..421eb3d3 100644 --- a/js/components/whitelabel/wallet/components/23vivi/23vivi_landing.js +++ b/js/components/whitelabel/wallet/components/23vivi/23vivi_landing.js @@ -26,7 +26,7 @@ let Vivi23Landing = React.createClass({ render() { return ( -
    +
    diff --git a/js/components/whitelabel/wallet/components/23vivi/23vivi_piece_list.js b/js/components/whitelabel/wallet/components/23vivi/23vivi_piece_list.js index bb64fc9a..e8bcabfc 100644 --- a/js/components/whitelabel/wallet/components/23vivi/23vivi_piece_list.js +++ b/js/components/whitelabel/wallet/components/23vivi/23vivi_piece_list.js @@ -1,4 +1,4 @@ -'use strict' +'use strict'; import React from 'react'; diff --git a/js/components/whitelabel/wallet/components/cyland/cyland_accordion_list/cyland_accordion_list_item.js b/js/components/whitelabel/wallet/components/cyland/cyland_accordion_list/cyland_accordion_list_item.js index 3ed7830e..0eb83d0c 100644 --- a/js/components/whitelabel/wallet/components/cyland/cyland_accordion_list/cyland_accordion_list_item.js +++ b/js/components/whitelabel/wallet/components/cyland/cyland_accordion_list/cyland_accordion_list_item.js @@ -46,8 +46,9 @@ let CylandAccordionListItem = React.createClass({ }, handleSubmitSuccess(response) { - PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, - this.state.orderBy, this.state.orderAsc, this.state.filterBy); + const { filterBy, orderAsc, orderBy, page, pageSize, search } = this.state; + + PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy }); const notification = new GlobalNotificationModel(response.notification, 'success', 10000); GlobalNotificationActions.appendGlobalNotification(notification); diff --git a/js/components/whitelabel/wallet/components/cyland/cyland_detail/cyland_piece_container.js b/js/components/whitelabel/wallet/components/cyland/cyland_detail/cyland_piece_container.js index d0315979..0b18ce9e 100644 --- a/js/components/whitelabel/wallet/components/cyland/cyland_detail/cyland_piece_container.js +++ b/js/components/whitelabel/wallet/components/cyland/cyland_detail/cyland_piece_container.js @@ -44,7 +44,7 @@ let CylandPieceContainer = React.createClass({ getInitialState() { return mergeOptions( - PieceStore.getState(), + PieceStore.getInitialState(), PieceListStore.getState() ); }, @@ -53,14 +53,17 @@ let CylandPieceContainer = React.createClass({ PieceStore.listen(this.onChange); PieceListStore.listen(this.onChange); - // Every time we enter the piece detail page, just reset the piece - // store as it will otherwise display wrong/old data once the user loads - // the piece detail a second time - PieceActions.updatePiece({}); - this.loadPiece(); }, + // We need this for when the user clicks on a notification while being in another piece view + componentWillReceiveProps(nextProps) { + if (this.props.params.pieceId !== nextProps.params.pieceId) { + PieceActions.flushPiece(); + this.loadPiece(); + } + }, + componentWillUnmount() { PieceStore.unlisten(this.onChange); PieceListStore.listen(this.onChange); @@ -71,12 +74,13 @@ let CylandPieceContainer = React.createClass({ }, loadPiece() { - PieceActions.fetchOne(this.props.params.pieceId); + PieceActions.fetchPiece(this.props.params.pieceId); }, handleDeleteSuccess(response) { - PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, - this.state.orderBy, this.state.orderAsc, this.state.filterBy); + const { filterBy, orderAsc, orderBy, page, pageSize, search } = this.state; + + PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy }); // since we're deleting a piece, we just need to close // all editions dialogs and not reload them @@ -90,10 +94,12 @@ let CylandPieceContainer = React.createClass({ }, render() { - if (this.state.piece && this.state.piece.id) { + const { piece } = this.state; + + if (piece.id) { const { currentUser } = this.props; - setDocumentTitle([this.state.piece.artist_name, this.state.piece.title].join(', ')); + setDocumentTitle(`${piece.artist_name}, ${piece.title}`); return ( ); - } - else { + } else { return (
    diff --git a/js/components/whitelabel/wallet/components/cyland/cyland_forms/cyland_additional_data_form.js b/js/components/whitelabel/wallet/components/cyland/cyland_forms/cyland_additional_data_form.js index 93cc515a..c41caa76 100644 --- a/js/components/whitelabel/wallet/components/cyland/cyland_forms/cyland_additional_data_form.js +++ b/js/components/whitelabel/wallet/components/cyland/cyland_forms/cyland_additional_data_form.js @@ -23,9 +23,10 @@ import { formSubmissionValidation } from '../../../../../ascribe_uploader/react_ let CylandAdditionalDataForm = React.createClass({ propTypes: { - handleSuccess: React.PropTypes.func, piece: React.PropTypes.object.isRequired, + disabled: React.PropTypes.bool, + handleSuccess: React.PropTypes.func, isInline: React.PropTypes.bool }, @@ -42,13 +43,13 @@ let CylandAdditionalDataForm = React.createClass({ }, handleSuccess() { - let notification = new GlobalNotificationModel(getLangText('Further details successfully updated'), 'success', 10000); + const notification = new GlobalNotificationModel(getLangText('Further details successfully updated'), 'success', 10000); GlobalNotificationActions.appendGlobalNotification(notification); }, getFormData() { - let extradata = {}; - let formRefs = this.refs.form.refs; + const extradata = {}; + const formRefs = this.refs.form.refs; // Put additional fields in extra data object Object @@ -71,10 +72,13 @@ let CylandAdditionalDataForm = React.createClass({ }, render() { - let { piece, isInline, disabled, handleSuccess, location } = this.props; - let buttons, spinner, heading; + const { disabled, handleSuccess, isInline, piece } = this.props; - if(!isInline) { + let buttons; + let spinner; + let heading; + + if (!isInline) { buttons = (
    ); @@ -101,13 +105,15 @@ let CylandAdditionalDataForm = React.createClass({ ); } - if(piece && piece.id) { + if (piece.id) { + const { extra_data: extraData = {} } = piece; + return (
    + expanded={!disabled || !!extraData.artist_bio}> + defaultValue={extraData.artist_bio} + placeholder={getLangText('Enter the artist\'s biography...')} /> + expanded={!disabled || !!extraData.artist_contact_information}> + convertLinks + defaultValue={extraData.artist_contact_information} + placeholder={getLangText('Enter the artist\'s contact information...')} /> + expanded={!disabled || !!extraData.conceptual_overview}> + defaultValue={extraData.conceptual_overview} + placeholder={getLangText('Enter a conceptual overview...')} /> + expanded={!disabled || !!extraData.medium}> + defaultValue={extraData.medium} + placeholder={getLangText('Enter the medium (and other technical specifications)...')} /> + expanded={!disabled || !!extraData.size_duration}> + defaultValue={extraData.size_duration} + placeholder={getLangText('Enter the size / duration...')} /> + expanded={!disabled || !!extraData.display_instructions}> + defaultValue={extraData.display_instructions} + placeholder={getLangText('Enter the display instructions...')} /> + expanded={!disabled || !!extraData.additional_details}> + defaultValue={extraData.additional_details} + placeholder={getLangText('Enter additional details...')} /> - +
    ); } diff --git a/js/components/whitelabel/wallet/components/cyland/cyland_landing.js b/js/components/whitelabel/wallet/components/cyland/cyland_landing.js index 7919fec4..197673fd 100644 --- a/js/components/whitelabel/wallet/components/cyland/cyland_landing.js +++ b/js/components/whitelabel/wallet/components/cyland/cyland_landing.js @@ -26,7 +26,7 @@ let CylandLanding = React.createClass({ setDocumentTitle('CYLAND MediaArtLab'); return ( -
    +
    diff --git a/js/components/whitelabel/wallet/components/cyland/cyland_piece_list.js b/js/components/whitelabel/wallet/components/cyland/cyland_piece_list.js index 76556139..1c2420d2 100644 --- a/js/components/whitelabel/wallet/components/cyland/cyland_piece_list.js +++ b/js/components/whitelabel/wallet/components/cyland/cyland_piece_list.js @@ -8,7 +8,6 @@ import CylandAccordionListItem from './cyland_accordion_list/cyland_accordion_li import { getLangText } from '../../../../../utils/lang_utils'; import { setDocumentTitle } from '../../../../../utils/dom_utils'; - let CylandPieceList = React.createClass({ propTypes: { // Provided from PrizeApp @@ -19,23 +18,36 @@ let CylandPieceList = React.createClass({ location: React.PropTypes.object }, + shouldRedirect(pieceCount) { + const { currentUser: { email: userEmail }, + whitelabel: { + user: whitelabelAdminEmail + } } = this.props; + + return userEmail !== whitelabelAdminEmail && !pieceCount; + }, + render() { setDocumentTitle(getLangText('Collection')); return ( -
    - -
    + ); } }); diff --git a/js/components/whitelabel/wallet/components/cyland/cyland_register_piece.js b/js/components/whitelabel/wallet/components/cyland/cyland_register_piece.js index 3c828299..7a0d2b66 100644 --- a/js/components/whitelabel/wallet/components/cyland/cyland_register_piece.js +++ b/js/components/whitelabel/wallet/components/cyland/cyland_register_piece.js @@ -49,7 +49,7 @@ let CylandRegisterPiece = React.createClass({ getInitialState(){ return mergeOptions( PieceListStore.getState(), - PieceStore.getState(), + PieceStore.getInitialState(), { step: 0 }); @@ -68,8 +68,8 @@ let CylandRegisterPiece = React.createClass({ // // We're using 'in' here as we want to know if 'piece_id' is present in the url, // we don't care about the value. - if (queryParams && 'piece_id' in queryParams) { - PieceActions.fetchOne(queryParams.piece_id); + if ('piece_id' in queryParams) { + PieceActions.fetchPiece(queryParams.piece_id); } }, @@ -82,31 +82,26 @@ let CylandRegisterPiece = React.createClass({ this.setState(state); }, - handleRegisterSuccess(response){ + handleRegisterSuccess(response) { this.refreshPieceList(); - // also start loading the piece for the next step + // Also load the newly registered piece for the next step if (response && response.piece) { - PieceActions.updatePiece({}); PieceActions.updatePiece(response.piece); } - this.incrementStep(); - - this.refs.slidesContainer.nextSlide({ piece_id: response.piece.id }); + this.nextSlide({ piece_id: response.piece.id }); }, handleAdditionalDataSuccess() { // We need to refetch the piece again after submitting the additional data - // since we want it's otherData to be displayed when the user choses to click + // since we want its otherData to be displayed when the user choses to click // on the browsers back button. - PieceActions.fetchOne(this.state.piece.id); + PieceActions.fetchPiece(this.state.piece.id); this.refreshPieceList(); - this.incrementStep(); - - this.refs.slidesContainer.nextSlide(); + this.nextSlide(); }, handleLoanSuccess(response) { @@ -115,29 +110,22 @@ let CylandRegisterPiece = React.createClass({ this.refreshPieceList(); - PieceActions.fetchOne(this.state.piece.id); - this.history.push(`/pieces/${this.state.piece.id}`); }, - // We need to increase the step to lock the forms that are already filled out - incrementStep() { - // also increase step - let newStep = this.state.step + 1; + nextSlide(queryParams) { + // We need to increase the step to lock the forms that are already filled out this.setState({ - step: newStep + step: this.state.step + 1 }); + + this.refs.slidesContainer.nextSlide(queryParams); }, refreshPieceList() { - PieceListActions.fetchPieceList( - this.state.page, - this.state.pageSize, - this.state.searchTerm, - this.state.orderBy, - this.state.orderAsc, - this.state.filterBy - ); + const { filterBy, orderAsc, orderBy, page, pageSize, search } = this.state; + + PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy }); }, render() { @@ -145,8 +133,7 @@ let CylandRegisterPiece = React.createClass({ const { piece, step } = this.state; const today = new Moment(); - const datetimeWhenWeAllWillBeFlyingCoolHoverboardsAndDinosaursWillLiveAgain = new Moment(); - datetimeWhenWeAllWillBeFlyingCoolHoverboardsAndDinosaursWillLiveAgain.add(1000, 'years'); + const datetimeWhenWeAllWillBeFlyingCoolHoverboardsAndDinosaursWillLiveAgain = new Moment().add(1000, 'years'); const loanHeading = getLangText('Loan to Cyland archive'); const loanButtons = ( diff --git a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_accordion_list/ikonotv_accordion_list_item.js b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_accordion_list/ikonotv_accordion_list_item.js index 53f04c41..41853e85 100644 --- a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_accordion_list/ikonotv_accordion_list_item.js +++ b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_accordion_list/ikonotv_accordion_list_item.js @@ -48,8 +48,9 @@ let IkonotvAccordionListItem = React.createClass({ }, handleSubmitSuccess(response) { - PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, - this.state.orderBy, this.state.orderAsc, this.state.filterBy); + const { filterBy, orderAsc, orderBy, page, pageSize, search } = this.state; + + PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy }); const notification = new GlobalNotificationModel(response.notification, 'success', 10000); GlobalNotificationActions.appendGlobalNotification(notification); diff --git a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_contract_notifications.js b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_contract_notifications.js index 60e5f10d..33f70aff 100644 --- a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_contract_notifications.js +++ b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_contract_notifications.js @@ -75,7 +75,7 @@ let IkonotvContractNotifications = React.createClass({ - Download contract + {getLangText('Download contract')}
    diff --git a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_detail/ikonotv_piece_container.js b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_detail/ikonotv_piece_container.js index 68e3c2eb..61a30572 100644 --- a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_detail/ikonotv_piece_container.js +++ b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_detail/ikonotv_piece_container.js @@ -8,12 +8,12 @@ import EditionListActions from '../../../../../../actions/edition_list_actions'; import GlobalNotificationModel from '../../../../../../models/global_notification_model'; import GlobalNotificationActions from '../../../../../../actions/global_notification_actions'; -import PieceActions from '../../../../../../actions/piece_actions'; -import PieceStore from '../../../../../../stores/piece_store'; - import PieceListStore from '../../../../../../stores/piece_list_store'; import PieceListActions from '../../../../../../actions/piece_list_actions'; +import PieceActions from '../../../../../../actions/piece_actions'; +import PieceStore from '../../../../../../stores/piece_store'; + import IkonotvSubmitButton from '../ikonotv_buttons/ikonotv_submit_button'; import IkonotvArtistDetailsForm from '../ikonotv_forms/ikonotv_artist_details_form'; @@ -45,8 +45,8 @@ let IkonotvPieceContainer = React.createClass({ getInitialState() { return mergeOptions( - PieceStore.getState(), - PieceListStore.getState() + PieceListStore.getState(), + PieceStore.getInitialState() ); }, @@ -54,19 +54,14 @@ let IkonotvPieceContainer = React.createClass({ PieceStore.listen(this.onChange); PieceListStore.listen(this.onChange); - // Every time we enter the piece detail page, just reset the piece - // store as it will otherwise display wrong/old data once the user loads - // the piece detail a second time - PieceActions.updatePiece({}); - this.loadPiece(); }, - // We need this for when the user clicks on a notification while being in another piece view + // We need this for when the user clicks on a notification while being in another piece view componentWillReceiveProps(nextProps) { if (this.props.params.pieceId !== nextProps.params.pieceId) { - PieceActions.updatePiece({}); - PieceActions.fetchOne(nextProps.params.pieceId); + PieceActions.flushPiece(); + this.loadPiece(); } }, @@ -80,12 +75,13 @@ let IkonotvPieceContainer = React.createClass({ }, loadPiece() { - PieceActions.fetchOne(this.props.params.pieceId); + PieceActions.fetchPiece(this.props.params.pieceId); }, handleDeleteSuccess(response) { - PieceListActions.fetchPieceList(this.state.page, this.state.pageSize, this.state.search, - this.state.orderBy, this.state.orderAsc, this.state.filterBy); + const { filterBy, orderAsc, orderBy, page, pageSize, search } = this.state; + + PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy }); // since we're deleting a piece, we just need to close // all editions dialogs and not reload them @@ -110,7 +106,7 @@ let IkonotvPieceContainer = React.createClass({ ); - if (piece.extra_data && Object.keys(piece.extra_data).length > 0 && piece.acl) { + if (piece.extra_data && Object.keys(piece.extra_data).length && piece.acl) { furtherDetails = ( + disabled={disabled}> {getLangText('Proceed to loan')} ); @@ -74,7 +76,7 @@ let IkonotvArtistDetailsForm = React.createClass({ spinner = (

    - +

    ); @@ -88,13 +90,15 @@ let IkonotvArtistDetailsForm = React.createClass({ ); } - if (this.props.piece && this.props.piece.id && this.props.piece.extra_data) { + if (piece.id) { + const { extra_data: extraData = {} } = piece; + return ( + expanded={!disabled || !!extraData.artist_website}> + convertLinks + defaultValue={extraData.artist_website} + placeholder={getLangText('The artist\'s website if present...')} /> + expanded={!disabled || !!extraData.gallery_website}> + convertLinks + defaultValue={extraData.gallery_website} + placeholder={getLangText('The website of any related Gallery or Museum')} /> + expanded={!disabled || !!extraData.additional_websites}> + convertLinks + defaultValue={extraData.additional_websites} + placeholder={getLangText('Enter additional Websites/Publications if any')} /> + expanded={!disabled || !!extraData.conceptual_overview}> + defaultValue={extraData.conceptual_overview} + placeholder={getLangText('Enter a short bio about the Artist')} /> ); diff --git a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_forms/ikonotv_artwork_details_form.js b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_forms/ikonotv_artwork_details_form.js index bcf6830d..28c67c94 100644 --- a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_forms/ikonotv_artwork_details_form.js +++ b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_forms/ikonotv_artwork_details_form.js @@ -20,11 +20,10 @@ import { getLangText } from '../../../../../../utils/lang_utils'; let IkonotvArtworkDetailsForm = React.createClass({ propTypes: { - handleSuccess: React.PropTypes.func, piece: React.PropTypes.object.isRequired, disabled: React.PropTypes.bool, - + handleSuccess: React.PropTypes.func, isInline: React.PropTypes.bool }, @@ -35,8 +34,8 @@ let IkonotvArtworkDetailsForm = React.createClass({ }, getFormData() { - let extradata = {}; - let formRefs = this.refs.form.refs; + const extradata = {}; + const formRefs = this.refs.form.refs; // Put additional fields in extra data object Object @@ -53,20 +52,23 @@ let IkonotvArtworkDetailsForm = React.createClass({ }, handleSuccess() { - let notification = new GlobalNotificationModel('Artwork details successfully updated', 'success', 10000); + const notification = new GlobalNotificationModel(getLangText('Artwork details successfully updated'), 'success', 10000); GlobalNotificationActions.appendGlobalNotification(notification); }, render() { - let buttons, spinner, heading; - let { isInline, handleSuccess } = this.props; + const { disabled, isInline, handleSuccess, piece } = this.props; + + let buttons; + let spinner; + let heading; if (!isInline) { buttons = ( ); @@ -74,7 +76,7 @@ let IkonotvArtworkDetailsForm = React.createClass({ spinner = (

    - +

    ); @@ -88,13 +90,15 @@ let IkonotvArtworkDetailsForm = React.createClass({ ); } - if (this.props.piece && this.props.piece.id && this.props.piece.extra_data) { + if (piece.id && piece.extra_data) { + const { extra_data: extraData = {} } = piece; + return (
    + expanded={!disabled || !!extraData.medium}> + defaultValue={extraData.medium} + placeholder={getLangText('The medium of the file (i.e. photo, video, other, ...)')} /> + expanded={!disabled || !!extraData.size_duration}> + defaultValue={extraData.size_duration} + placeholder={getLangText('Size in centimeters. Duration in minutes.')} /> + expanded={!disabled || !!extraData.copyright}> + defaultValue={extraData.copyright} + placeholder={getLangText('Which copyright is attached to this work?')} /> + expanded={!disabled || !!extraData.courtesy_of}> + defaultValue={extraData.courtesy_of} + placeholder={getLangText('The current owner of the artwork')} /> + expanded={!disabled || !!extraData.copyright_of_photography}> + defaultValue={extraData.copyright_of_photography} + placeholder={getLangText('Who should be attributed for the photography?')} /> + expanded={!disabled || !!extraData.additional_details}> + defaultValue={extraData.additional_details} + placeholder={getLangText('Insert artwork overview')} /> ); diff --git a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_piece_list.js b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_piece_list.js index 4661cc32..d1811994 100644 --- a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_piece_list.js +++ b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_piece_list.js @@ -38,10 +38,17 @@ let IkonotvPieceList = React.createClass({ this.setState(state); }, - redirectIfNoContractNotifications() { + shouldRedirect(pieceCount) { + const { currentUser: { email: userEmail }, + whitelabel: { + user: whitelabelAdminEmail + } } = this.props; const { contractAgreementListNotifications } = this.state; - return contractAgreementListNotifications && !contractAgreementListNotifications.length; + return contractAgreementListNotifications && + !contractAgreementListNotifications.length && + userEmail !== whitelabelAdminEmail && + !pieceCount; }, render() { @@ -51,8 +58,6 @@ let IkonotvPieceList = React.createClass({
    + }]} + redirectTo={{ + pathname: '/register_piece', + query: { + 'slide_num': 0 + } + }} + shouldRedirect={this.shouldRedirect} />
    ); } diff --git a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_register_piece.js b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_register_piece.js index 466aa9fe..3684fb29 100644 --- a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_register_piece.js +++ b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_register_piece.js @@ -47,7 +47,7 @@ let IkonotvRegisterPiece = React.createClass({ getInitialState() { return mergeOptions( PieceListStore.getState(), - PieceStore.getState(), + PieceStore.getInitialState(), { step: 0, pageExitWarning: getLangText("If you leave this form now, your work will not be loaned to Ikono TV.") @@ -59,9 +59,7 @@ let IkonotvRegisterPiece = React.createClass({ PieceListStore.listen(this.onChange); PieceStore.listen(this.onChange); - // Before we load the new piece, we reset the piece store to delete old data that we do - // not want to display to the user. - PieceActions.updatePiece({}); + const queryParams = this.props.location.query; // Since every step of this register process is atomic, // we may need to enter the process at step 1 or 2. @@ -70,9 +68,8 @@ let IkonotvRegisterPiece = React.createClass({ // // We're using 'in' here as we want to know if 'piece_id' is present in the url, // we don't care about the value. - const queryParams = this.props.location.query; - if (queryParams && 'piece_id' in queryParams) { - PieceActions.fetchOne(queryParams.piece_id); + if ('piece_id' in queryParams) { + PieceActions.fetchPiece(queryParams.piece_id); } }, @@ -86,33 +83,30 @@ let IkonotvRegisterPiece = React.createClass({ }, - handleRegisterSuccess(response){ + handleRegisterSuccess(response) { this.refreshPieceList(); - // also start loading the piece for the next step + // Also load the newly registered piece for the next step if (response && response.piece) { PieceActions.updatePiece(response.piece); } + if (!this.canSubmit()) { this.history.push('/collection'); } else { - this.incrementStep(); - this.refs.slidesContainer.nextSlide(); + this.nextSlide({ piece_id: response.piece.id }); } }, handleAdditionalDataSuccess() { - // We need to refetch the piece again after submitting the additional data // since we want it's otherData to be displayed when the user choses to click // on the browsers back button. - PieceActions.fetchOne(this.state.piece.id); + PieceActions.fetchPiece(this.state.piece.id); this.refreshPieceList(); - this.incrementStep(); - - this.refs.slidesContainer.nextSlide(); + this.nextSlide(); }, handleLoanSuccess(response) { @@ -123,33 +117,27 @@ let IkonotvRegisterPiece = React.createClass({ this.refreshPieceList(); - PieceActions.fetchOne(this.state.piece.id); this.history.push(`/pieces/${this.state.piece.id}`); }, - // We need to increase the step to lock the forms that are already filled out - incrementStep() { - // also increase step - let newStep = this.state.step + 1; + nextSlide(queryParams) { + // We need to increase the step to lock the forms that are already filled out this.setState({ - step: newStep + step: this.state.step + 1 }); + + this.refs.slidesContainer.nextSlide(queryParams); }, refreshPieceList() { - PieceListActions.fetchPieceList( - this.state.page, - this.state.pageSize, - this.state.searchTerm, - this.state.orderBy, - this.state.orderAsc, - this.state.filterBy - ); + const { filterBy, orderAsc, orderBy, page, pageSize, search } = this.state; + + PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, filterBy }); }, canSubmit() { const { currentUser, whitelabel } = this.props; - return currentUser && currentUser.acl && currentUser.acl.acl_wallet_submit && whitelabel && whitelabel.user; + return currentUser.acl && currentUser.acl.acl_wallet_submit && whitelabel.user; }, getSlideArtistDetails() { @@ -160,13 +148,14 @@ let IkonotvRegisterPiece = React.createClass({ + piece={this.state.piece} />
    ); + } else { + return null; } - return null; }, getSlideArtworkDetails() { @@ -177,13 +166,14 @@ let IkonotvRegisterPiece = React.createClass({ + piece={this.state.piece} />
    ); + } else { + return null; } - return null; }, getSlideLoan() { @@ -214,8 +204,9 @@ let IkonotvRegisterPiece = React.createClass({
    ); + } else { + return null; } - return null; }, render() { diff --git a/js/components/whitelabel/wallet/components/market/market_buttons/market_submit_button.js b/js/components/whitelabel/wallet/components/market/market_buttons/market_submit_button.js index c0f58380..8892a856 100644 --- a/js/components/whitelabel/wallet/components/market/market_buttons/market_submit_button.js +++ b/js/components/whitelabel/wallet/components/market/market_buttons/market_submit_button.js @@ -3,7 +3,7 @@ import React from 'react'; import classNames from 'classnames'; -import PieceActions from '../../../../../../actions/piece_actions'; +import EditionActions from '../../../../../../actions/edition_actions'; import MarketAdditionalDataForm from '../market_forms/market_additional_data_form'; @@ -24,30 +24,28 @@ let MarketSubmitButton = React.createClass({ availableAcls: React.PropTypes.object.isRequired, currentUser: React.PropTypes.object.isRequired, editions: React.PropTypes.array.isRequired, - handleSuccess: React.PropTypes.func.isRequired, - whitelabel: React.PropTypes.object.isRequired, className: React.PropTypes.string, + handleSuccess: React.PropTypes.func }, canEditionBeSubmitted(edition) { if (edition && edition.extra_data && edition.other_data) { - const { extra_data, other_data } = edition; + const { + extra_data: { + artist_bio: artistBio, + display_instructions: displayInstructions, + technology_details: technologyDetails, + work_description: workDescription + }, + other_data: otherData } = edition; - if (extra_data.artist_bio && extra_data.work_description && - extra_data.technology_details && extra_data.display_instructions && - other_data.length > 0) { - return true; - } + return artistBio && displayInstructions && technologyDetails && workDescription && otherData.length; } return false; }, - getFormDataId() { - return getAclFormDataId(false, this.props.editions); - }, - getAggregateEditionDetails() { const { editions } = this.props; @@ -64,13 +62,20 @@ let MarketSubmitButton = React.createClass({ }); }, - handleAdditionalDataSuccess(pieceId) { - // Fetch newly updated piece to update the views - PieceActions.fetchOne(pieceId); + getFormDataId() { + return getAclFormDataId(false, this.props.editions); + }, + handleAdditionalDataSuccess() { this.refs.consignModal.show(); }, + refreshEdition() { + if (this.props.editions.length === 1) { + EditionActions.fetchEdition(this.props.editions[0].bitcoin_id); + } + }, + render() { const { availableAcls, @@ -89,6 +94,10 @@ let MarketSubmitButton = React.createClass({ senderName: currentUser.username }); + // If only a single piece is selected, all the edition's extra_data and other_data will + // be the same, so we just take the first edition's + const { extra_data: extraData, other_data: otherData } = solePieceId ? editions[0] : {}; + const triggerButton = (