diff --git a/.gitignore b/.gitignore index f5bf11e8..e497465f 100644 --- a/.gitignore +++ b/.gitignore @@ -16,10 +16,14 @@ webapp-dependencies.txt pids logs results - -node_modules/* -build +build/* + +gemini-coverage/* +gemini-report/* +test/gemini/screenshots/* + +node_modules/* .DS_Store .env diff --git a/README.md b/README.md index af64cc3c..594135dd 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 @@ -38,17 +39,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: @@ -62,22 +80,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. @@ -87,7 +104,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`. @@ -138,9 +172,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 ============ @@ -153,7 +194,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/event_actions.js b/js/actions/event_actions.js index 6d8ee12f..24889f4e 100644 --- a/js/actions/event_actions.js +++ b/js/actions/event_actions.js @@ -8,9 +8,8 @@ class EventActions { this.generateActions( 'applicationWillBoot', 'applicationDidBoot', - 'profileDidLoad', - //'userDidLogin', - //'userDidLogout', + 'userDidAuthenticate', + 'userDidLogout', 'routeDidChange' ); } diff --git a/js/actions/user_actions.js b/js/actions/user_actions.js index a661b8de..9d59044f 100644 --- a/js/actions/user_actions.js +++ b/js/actions/user_actions.js @@ -1,6 +1,6 @@ 'use strict'; -import { altUser } from '../alt'; +import { alt } from '../alt'; class UserActions { @@ -15,4 +15,4 @@ class UserActions { } } -export default altUser.createActions(UserActions); +export default alt.createActions(UserActions); diff --git a/js/alt.js b/js/alt.js index 141248c1..3e1d3fae 100644 --- a/js/alt.js +++ b/js/alt.js @@ -4,5 +4,4 @@ import Alt from 'alt'; export let alt = new Alt(); export let altThirdParty = new Alt(); -export let altUser = new Alt(); export let altWhitelabel = new Alt(); 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 new file mode 100644 index 00000000..d680faeb --- /dev/null +++ b/js/components/app_route_wrapper.js @@ -0,0 +1,34 @@ +'use strict'; + +import React from 'react'; + +import { omitFromObject } from '../utils/general_utils'; + +const AppRouteWrapper = React.createClass({ + propTypes: { + children: React.PropTypes.oneOfType([ + React.PropTypes.arrayOf(React.PropTypes.element), + React.PropTypes.element + ]).isRequired + }, + + render() { + const propsToPropagate = omitFromObject(this.props, ['children']); + + let childrenWithProps = this.props.children; + // If there are more props given, propagate them into the child routes by cloning the routes + if (Object.keys(propsToPropagate).length) { + childrenWithProps = React.Children.map(this.props.children, (child) => { + return React.cloneElement(child, propsToPropagate); + }); + } + + return ( +
+ {childrenWithProps} +
+ ); + } +}); + +export default AppRouteWrapper; diff --git a/js/components/ascribe_accordion_list/accordion_list.js b/js/components/ascribe_accordion_list/accordion_list.js index 1046ab7f..1ce113cb 100644 --- a/js/components/ascribe_accordion_list/accordion_list.js +++ b/js/components/ascribe_accordion_list/accordion_list.js @@ -1,14 +1,17 @@ 'use strict'; import React from 'react'; +import { Link } from 'react-router'; + 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, @@ -22,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} @@ -36,7 +39,7 @@ let AccordionList = React.createClass({

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

); 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 f6712d37..e752451e 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'; @@ -29,50 +28,51 @@ import { mergeOptions } from '../../utils/general_utils'; let AccordionListItemWallet = React.createClass({ propTypes: { - className: React.PropTypes.string, - content: React.PropTypes.object, - thumbnailPlaceholder: React.PropTypes.func, + content: React.PropTypes.object.isRequired, + whitelabel: React.PropTypes.object.isRequired, + children: React.PropTypes.oneOfType([ React.PropTypes.arrayOf(React.PropTypes.element), React.PropTypes.element - ]) + ]), + className: React.PropTypes.string, + thumbnailPlaceholder: React.PropTypes.func }, 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) { 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 ( {getLangText('You have actions pending')}}> - - ); + + + ); + } else { + return null; } - return null; }, toggleCreateEditionsDialog() { @@ -93,7 +93,7 @@ let AccordionListItemWallet = React.createClass({ PieceListActions.fetchPieceList({ page, pageSize, search, orderBy, orderAsc, 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); }, @@ -111,13 +111,15 @@ let AccordionListItemWallet = React.createClass({ }, getLicences() { + const { content, whitelabel } = this.props; + // convert this to acl_view_licences later - if (this.state.whitelabel && this.state.whitelabel.name === 'Creative Commons France') { + if (whitelabel.name === 'Creative Commons France') { return ( , - - {getLangText('%s license', this.props.content.license_type.code)} + + {getLangText('%s license', content.license_type.code)} ); diff --git a/js/components/ascribe_app.js b/js/components/ascribe_app.js index cda5637f..87ab1daf 100644 --- a/js/components/ascribe_app.js +++ b/js/components/ascribe_app.js @@ -2,36 +2,42 @@ import React from 'react'; -import Header from '../components/header'; -import Footer from '../components/footer'; -import GlobalNotification from './global_notification'; +import AppBase from './app_base'; +import AppRouteWrapper from './app_route_wrapper'; +import Footer from './footer'; +import Header from './header'; let AscribeApp = React.createClass({ propTypes: { - children: React.PropTypes.oneOfType([ - React.PropTypes.arrayOf(React.PropTypes.element), - React.PropTypes.element - ]), - routes: React.PropTypes.arrayOf(React.PropTypes.object) + activeRoute: React.PropTypes.object.isRequired, + children: React.PropTypes.element.isRequired, + routes: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, + + // Provided from AppBase + currentUser: React.PropTypes.object, + whitelabel: React.PropTypes.object }, render() { - let { children, routes } = this.props; + const { activeRoute, children, currentUser, routes, whitelabel } = this.props; return ( -
-
- {/* Routes are injected here */} -
+
+
+ + {/* Routes are injected here */} {children} -
-
- - + +
); } }); -export default AscribeApp; +export default AppBase(AscribeApp); diff --git a/js/components/ascribe_buttons/acl_button_list.js b/js/components/ascribe_buttons/acl_button_list.js index 35e42c20..d059e0f2 100644 --- a/js/components/ascribe_buttons/acl_button_list.js +++ b/js/components/ascribe_buttons/acl_button_list.js @@ -2,9 +2,6 @@ import React from 'react/addons'; -import UserActions from '../../actions/user_actions'; -import UserStore from '../../stores/user_store'; - import ConsignButton from './acls/consign_button'; import EmailButton from './acls/email_button'; import LoanButton from './acls/loan_button'; @@ -12,50 +9,44 @@ import LoanRequestButton from './acls/loan_request_button'; import TransferButton from './acls/transfer_button'; import UnconsignButton from './acls/unconsign_button'; -import { mergeOptions } from '../../utils/general_utils'; +import { selectFromObject } from '../../utils/general_utils'; let AclButtonList = React.createClass({ propTypes: { - className: React.PropTypes.string, + availableAcls: React.PropTypes.object.isRequired, + currentUser: React.PropTypes.object.isRequired, + handleSuccess: React.PropTypes.func.isRequired, pieceOrEditions: React.PropTypes.oneOfType([ React.PropTypes.object, React.PropTypes.array ]).isRequired, - availableAcls: React.PropTypes.object.isRequired, + buttonsStyle: React.PropTypes.object, - handleSuccess: React.PropTypes.func.isRequired, children: React.PropTypes.oneOfType([ React.PropTypes.arrayOf(React.PropTypes.element), React.PropTypes.element - ]) + ]), + className: React.PropTypes.string }, getInitialState() { - return mergeOptions( - UserStore.getState(), - { - buttonListSize: 0 - } - ); + return { + buttonListSize: 0 + } }, componentDidMount() { - UserStore.listen(this.onChange); - UserActions.fetchCurrentUser.defer(); - window.addEventListener('resize', this.handleResize); window.dispatchEvent(new Event('resize')); }, componentDidUpdate(prevProps) { - if(prevProps.availableAcls && prevProps.availableAcls !== this.props.availableAcls) { + if (prevProps.availableAcls && prevProps.availableAcls !== this.props.availableAcls) { window.dispatchEvent(new Event('resize')); } }, componentWillUnmount() { - UserStore.unlisten(this.onChange); - window.removeEventListener('resize', this.handleResize); }, @@ -65,10 +56,6 @@ let AclButtonList = React.createClass({ }); }, - onChange(state) { - this.setState(state); - }, - renderChildren() { const { children } = this.props; const { buttonListSize } = this.state; @@ -79,42 +66,29 @@ let AclButtonList = React.createClass({ }, render() { - const { className, - buttonsStyle, - availableAcls, - pieceOrEditions, - handleSuccess } = this.props; + const { + availableAcls, + buttonsStyle, + className, + currentUser, + handleSuccess, + pieceOrEditions } = this.props; - const { currentUser } = this.state; + const buttonProps = selectFromObject(this.props, [ + 'availableAcls', + 'currentUser', + 'handleSuccess', + 'pieceOrEditions' + ]); return (
- - - - - + + + + + {this.renderChildren()}
diff --git a/js/components/ascribe_buttons/acls/acl_button.js b/js/components/ascribe_buttons/acls/acl_button.js index 97f2e173..d40a779a 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.'); } @@ -24,23 +24,20 @@ export default function ({ action, displayName, title, tooltip }) { propTypes: { availableAcls: React.PropTypes.object.isRequired, - buttonAcceptName: React.PropTypes.string, - buttonAcceptClassName: React.PropTypes.string, - currentUser: React.PropTypes.object, - email: React.PropTypes.string, pieceOrEditions: React.PropTypes.oneOfType([ React.PropTypes.object, React.PropTypes.array ]).isRequired, - handleSuccess: React.PropTypes.func.isRequired, - className: React.PropTypes.string + + buttonAcceptName: React.PropTypes.string, + buttonAcceptClassName: React.PropTypes.string, + currentUser: React.PropTypes.object, + email: React.PropTypes.string, + handleSuccess: React.PropTypes.func }, sanitizeAction() { - if (this.props.buttonAcceptName) { - return this.props.buttonAcceptName; - } - return AclInformationText.titles[action]; + return this.props.buttonAcceptName || AclInformationText.titles[action]; }, render() { diff --git a/js/components/ascribe_buttons/unconsign_request_button.js b/js/components/ascribe_buttons/unconsign_request_button.js index e5e1c661..c324ff28 100644 --- a/js/components/ascribe_buttons/unconsign_request_button.js +++ b/js/components/ascribe_buttons/unconsign_request_button.js @@ -15,10 +15,12 @@ let UnConsignRequestButton = React.createClass({ propTypes: { currentUser: React.PropTypes.object.isRequired, edition: React.PropTypes.object.isRequired, - handleSuccess: React.PropTypes.func.isRequired + + handleSuccess: React.PropTypes.func }, render: function () { + const { currentUser, edition, handleSuccess } = this.props; return ( } - handleSuccess={this.props.handleSuccess} + handleSuccess={handleSuccess} title='Request to Un-Consign'> +${currentUser.username}` + } /> ); } diff --git a/js/components/ascribe_detail/edition.js b/js/components/ascribe_detail/edition.js index df9d41a0..f2110e10 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'; @@ -36,11 +36,13 @@ 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, - furtherDetailsType: React.PropTypes.func, - edition: React.PropTypes.object, coaError: React.PropTypes.object, - currentUser: React.PropTypes.object, + furtherDetailsType: React.PropTypes.func, loadEdition: React.PropTypes.func }, @@ -57,56 +59,56 @@ let Edition = React.createClass({ currentUser, edition, furtherDetailsType: FurtherDetailsType, - loadEdition } = this.props; + loadEdition, + whitelabel } = this.props; return ( + currentUser={currentUser} + 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)')} @@ -130,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}> - - + +
@@ -158,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} />
@@ -360,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 71bf38fe..dd5c117b 100644 --- a/js/components/ascribe_detail/edition_action_panel.js +++ b/js/components/ascribe_detail/edition_action_panel.js @@ -36,9 +36,11 @@ import { getLangText } from '../../utils/lang_utils'; */ let EditionActionPanel = 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 }, @@ -87,39 +89,42 @@ 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 && - edition.notifications && - edition.notifications.length > 0){ + if (edition.notifications && edition.notifications.length) { return ( ); - } - - else { + notifications={edition.notifications} + pieceOrEditions={[edition]} + handleSuccess={this.handleSuccess} />); + } else { return ( + whitelabel={whitelabel}> diff --git a/js/components/ascribe_detail/edition_container.js b/js/components/ascribe_detail/edition_container.js index d0adadf0..77ebed48 100644 --- a/js/components/ascribe_detail/edition_container.js +++ b/js/components/ascribe_detail/edition_container.js @@ -9,16 +9,12 @@ import { ResourceNotFoundError } from '../../models/errors'; import EditionActions from '../../actions/edition_actions'; import EditionStore from '../../stores/edition_store'; -import UserActions from '../../actions/user_actions'; -import UserStore from '../../stores/user_store'; - import Edition from './edition'; import AscribeSpinner from '../ascribe_spinner'; import { getLangText } from '../../utils/lang_utils'; import { setDocumentTitle } from '../../utils/dom_utils'; -import { mergeOptions } from '../../utils/general_utils'; /** @@ -28,24 +24,26 @@ let EditionContainer = React.createClass({ propTypes: { actionPanelButtonListType: React.PropTypes.func, furtherDetailsType: React.PropTypes.func, + + // Provided from AscribeApp + currentUser: React.PropTypes.object.isRequired, + whitelabel: React.PropTypes.object.isRequired, + + // Provided from router + location: React.PropTypes.object, params: React.PropTypes.object }, mixins: [History, ReactError], getInitialState() { - return mergeOptions( - EditionStore.getInitialState(), - UserStore.getState() - ); + return EditionStore.getInitialState(); }, componentDidMount() { EditionStore.listen(this.onChange); - UserStore.listen(this.onChange); this.loadEdition(); - UserActions.fetchCurrentUser(); }, // This is done to update the container when the user clicks on the prev or next @@ -68,19 +66,10 @@ let EditionContainer = React.createClass({ componentWillUnmount() { window.clearInterval(this.state.timerId); EditionStore.unlisten(this.onChange); - UserStore.unlisten(this.onChange); }, onChange(state) { this.setState(state); - - if(state && state.edition && state.edition.digital_work) { - let 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.fetchEdition(this.props.params.editionId), 10000); - this.setState({timerId: timerId}); - } - } }, loadEdition(editionId = this.props.params.editionId) { @@ -88,8 +77,8 @@ let EditionContainer = React.createClass({ }, render() { - const { edition, currentUser, coaMeta } = this.state; - const { actionPanelButtonListType, furtherDetailsType } = this.props; + const { actionPanelButtonListType, currentUser, furtherDetailsType, whitelabel } = this.props; + const { edition, coaMeta } = this.state; if (edition.id) { setDocumentTitle(`${edition.artist_name}, ${edition.title}`); @@ -97,11 +86,12 @@ let EditionContainer = React.createClass({ return ( + edition={edition} + furtherDetailsType={furtherDetailsType} + loadEdition={this.loadEdition} + whitelabel={whitelabel} /> ); } else { return ( diff --git a/js/components/ascribe_detail/media_container.js b/js/components/ascribe_detail/media_container.js index 00ca9164..1e9ba0a1 100644 --- a/js/components/ascribe_detail/media_container.js +++ b/js/components/ascribe_detail/media_container.js @@ -22,12 +22,14 @@ const EMBED_IFRAME_HEIGHT = { video: 315, audio: 62 }; +const ENCODE_UPDATE_TIME = 5000; 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() { @@ -37,14 +39,16 @@ let MediaContainer = React.createClass({ }, componentDidMount() { - if (!this.props.content.digital_work) { - return; - } + const { content: { digital_work: digitalWork }, refreshObject } = this.props; - 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); - this.setState({timerId: timerId}); + if (digitalWork) { + const isEncoding = digitalWork.isEncoding; + + if (digitalWork.mime === 'video' && typeof isEncoding === 'number' && isEncoding !== 100 && !this.state.timerId) { + this.setState({ + timerId: window.setInterval(refreshObject, ENCODE_UPDATE_TIME) + }); + } } }, @@ -105,7 +109,7 @@ let MediaContainer = React.createClass({ {''} - }/> + } /> ); } return ( @@ -136,7 +140,7 @@ let MediaContainer = React.createClass({ If it turns out that `fileExtension` is an empty string, we're just using the label 'file'. */} - {getLangText('Download')} .{fileExtension || 'file'} + {getLangText('Download')} .{fileExtension || 'file'} {embed} diff --git a/js/components/ascribe_detail/note.js b/js/components/ascribe_detail/note.js index c739b937..693a0400 100644 --- a/js/components/ascribe_detail/note.js +++ b/js/components/ascribe_detail/note.js @@ -2,64 +2,68 @@ import React from 'react'; +import GlobalNotificationModel from '../../models/global_notification_model'; +import GlobalNotificationActions from '../../actions/global_notification_actions'; + import Form from './../ascribe_forms/form'; import Property from './../ascribe_forms/property'; import InputTextAreaToggable from './../ascribe_forms/input_textarea_toggable'; -import GlobalNotificationModel from '../../models/global_notification_model'; -import GlobalNotificationActions from '../../actions/global_notification_actions'; - import { getLangText } from '../../utils/lang_utils'; let Note = React.createClass({ propTypes: { - url: React.PropTypes.string, - id: React.PropTypes.func, - label: React.PropTypes.string, - currentUser: React.PropTypes.object, + currentUser: React.PropTypes.object.isRequired, + id: React.PropTypes.func.isRequired, + url: React.PropTypes.string.isRequired, + defaultValue: React.PropTypes.string, editable: React.PropTypes.bool, - show: React.PropTypes.bool, + label: React.PropTypes.string, placeholder: React.PropTypes.string, + show: React.PropTypes.bool, successMessage: React.PropTypes.string }, getDefaultProps() { return { editable: true, - show: true, placeholder: getLangText('Enter a note'), + show: true, successMessage: getLangText('Note saved') }; }, - showNotification(){ - let notification = new GlobalNotificationModel(this.props.successMessage, 'success'); + showNotification() { + const notification = new GlobalNotificationModel(this.props.successMessage, 'success'); GlobalNotificationActions.appendGlobalNotification(notification); }, render() { - if ((!!this.props.currentUser.username && this.props.editable || !this.props.editable ) && this.props.show) { + const { currentUser, defaultValue, editable, id, label, placeholder, show, url } = this.props; + + if ((!!currentUser.username && editable || !editable ) && show) { return (
+ disabled={!editable}> + label={label}> + defaultValue={defaultValue} + placeholder={placeholder} />
); + } else { + return null; } - return null; } }); -export default Note; \ No newline at end of file +export default Note; diff --git a/js/components/ascribe_detail/piece.js b/js/components/ascribe_detail/piece.js index e4ff4ea7..9864cf95 100644 --- a/js/components/ascribe_detail/piece.js +++ b/js/components/ascribe_detail/piece.js @@ -15,19 +15,19 @@ import MediaContainer from './media_container'; */ let Piece = React.createClass({ propTypes: { - piece: React.PropTypes.object, + piece: React.PropTypes.object.isRequired, + + buttons: React.PropTypes.object, currentUser: React.PropTypes.object, header: React.PropTypes.object, subheader: React.PropTypes.object, - buttons: React.PropTypes.object, children: React.PropTypes.oneOfType([ React.PropTypes.arrayOf(React.PropTypes.element), React.PropTypes.element ]) }, - - updateObject() { + updatePiece() { return PieceActions.fetchPiece(this.props.piece.id); }, @@ -40,7 +40,7 @@ let Piece = React.createClass({ + refreshObject={this.updatePiece} /> {header} diff --git a/js/components/ascribe_detail/piece_container.js b/js/components/ascribe_detail/piece_container.js index 8ee3111f..0f9df8f6 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'; @@ -54,6 +50,13 @@ import { setDocumentTitle } from '../../utils/dom_utils'; let PieceContainer = React.createClass({ propTypes: { furtherDetailsType: React.PropTypes.func, + + // Provided from AscribeApp + currentUser: React.PropTypes.object.isRequired, + whitelabel: React.PropTypes.object, + + // Provided from router + location: React.PropTypes.object, params: React.PropTypes.object }, @@ -67,7 +70,6 @@ let PieceContainer = React.createClass({ getInitialState() { return mergeOptions( - UserStore.getState(), PieceListStore.getState(), PieceStore.getInitialState(), { @@ -77,12 +79,10 @@ let PieceContainer = React.createClass({ }, componentDidMount() { - UserStore.listen(this.onChange); PieceListStore.listen(this.onChange); PieceStore.listen(this.onChange); this.loadPiece(); - UserActions.fetchCurrentUser(); }, // This is done to update the container when the user clicks on the prev or next @@ -105,7 +105,6 @@ let PieceContainer = React.createClass({ componentWillUnmount() { PieceStore.unlisten(this.onChange); - UserStore.unlisten(this.onChange); PieceListStore.unlisten(this.onChange); }, @@ -207,15 +206,17 @@ let PieceContainer = React.createClass({ }, getActions() { - const { piece, currentUser } = this.state; + const { piece } = this.state; + const { currentUser } = this.props; if (piece.notifications && piece.notifications.length > 0) { return ( ); + notifications={piece.notifications} + pieceOrEditions={piece} /> + ); } else { return ( ); } else if (action === 'acl_loan_request') { @@ -122,7 +123,7 @@ let AclFormFactory = React.createClass({ message={formMessage} id={this.getFormDataId()} url={this.isPiece() ? ApiUrls.ownership_shares_pieces - : ApiUrls.ownership_shares_editions} + : ApiUrls.ownership_shares_editions} handleSuccess={showNotification ? this.showSuccessNotification : handleSuccess} /> ); } else { 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..f9b68f48 100644 --- a/js/components/ascribe_forms/form_copyright_association.js +++ b/js/components/ascribe_forms/form_copyright_association.js @@ -15,30 +15,30 @@ import { getLangText } from '../../utils/lang_utils'; let CopyrightAssociationForm = React.createClass({ propTypes: { - currentUser: React.PropTypes.object + currentUser: React.PropTypes.object.isRequired }, - handleSubmitSuccess(){ - let notification = getLangText('Copyright association updated'); - notification = new GlobalNotificationModel(notification, 'success', 10000); + handleSubmitSuccess() { + const notification = new GlobalNotificationModel(getLangText('Copyright association updated'), 'success', 10000); GlobalNotificationActions.appendGlobalNotification(notification); }, - getProfileFormData(){ - return {email: this.props.currentUser.email}; + getProfileFormData() { + return { email: this.props.currentUser.email }; }, render() { - let selectedState; - let selectDefaultValue = ' -- ' + getLangText('select an association') + ' -- '; + const { currentUser } = this.props; + const selectDefaultValue = ' -- ' + getLangText('select an association') + ' -- '; - if (this.props.currentUser && this.props.currentUser.profile - && this.props.currentUser.profile.copyright_association) { - selectedState = AppConstants.copyrightAssociations.indexOf(this.props.currentUser.profile.copyright_association); - selectedState = selectedState !== -1 ? AppConstants.copyrightAssociations[selectedState] : selectDefaultValue; + let selectedState = selectDefaultValue; + if (currentUser.profile && currentUser.profile.copyright_association) { + if (AppConstants.copyrightAssociations.indexOf(currentUser.profile.copyright_association) !== -1) { + selectedState = AppConstants.copyrightAssociations[selectedState]; + } } - if (this.props.currentUser && this.props.currentUser.email){ + if (currentUser.email) { return (
+ label={getLangText('Copyright Association')}> + onChange={this.handleCheckboxToggle} + type="checkbox" /> {' ' + checkboxLabel}
); diff --git a/js/components/ascribe_modal/modal_wrapper.js b/js/components/ascribe_modal/modal_wrapper.js index 511e7f8c..53f1c90b 100644 --- a/js/components/ascribe_modal/modal_wrapper.js +++ b/js/components/ascribe_modal/modal_wrapper.js @@ -6,6 +6,10 @@ import Modal from 'react-bootstrap/lib/Modal'; let ModalWrapper = React.createClass({ propTypes: { + children: React.PropTypes.oneOfType([ + React.PropTypes.arrayOf(React.PropTypes.element), + React.PropTypes.element + ]).isRequired, title: React.PropTypes.oneOfType([ React.PropTypes.arrayOf(React.PropTypes.element), React.PropTypes.element, @@ -14,11 +18,7 @@ let ModalWrapper = React.createClass({ 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 - ]) + trigger: React.PropTypes.element }, getInitialState() { 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_routes/proxy_handler.js b/js/components/ascribe_routes/proxy_handler.js index 7752912a..52084e6b 100644 --- a/js/components/ascribe_routes/proxy_handler.js +++ b/js/components/ascribe_routes/proxy_handler.js @@ -5,7 +5,6 @@ import { RouteContext } from 'react-router'; import history from '../../history'; import UserStore from '../../stores/user_store'; -import UserActions from '../../actions/user_actions'; import AppConstants from '../../constants/application_constants'; @@ -21,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`); } @@ -35,23 +34,22 @@ export function AuthRedirect({to, when}) { // // So if when === 'loggedIn', we're checking if the user is logged in (and // vice versa) - let exprToValidate = when === 'loggedIn' ? currentUser && currentUser.email - : currentUser && !currentUser.email; + const isLoggedIn = Object.keys(currentUser).length && currentUser.email; + const exprToValidate = when === 'loggedIn' ? isLoggedIn : !isLoggedIn; // and redirect if `true`. - if(exprToValidate) { + if (exprToValidate) { 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) { - + } else if (!exprToValidate && when === 'loggedIn' && redirect) { delete query.redirect; window.setTimeout(() => history.replace({ query, pathname: '/' + redirect })); return true; - } else if(!exprToValidate && when === 'loggedOut' && redirectAuthenticated) { + } else if (!exprToValidate && when === 'loggedOut' && redirectAuthenticated) { /* * redirectAuthenticated contains an arbitrary path * eg pieces/, editions/, collection, settings, ... @@ -64,6 +62,7 @@ export function AuthRedirect({to, when}) { window.location = AppConstants.baseUrl + redirectAuthenticated; return true; } + return false; }; } @@ -81,6 +80,11 @@ export function ProxyHandler(...redirectFunctions) { displayName: 'ProxyHandler', propTypes: { + // Provided from AscribeApp + currentUser: React.PropTypes.object.isRequired, + whitelabel: React.PropTypes.object, + + // Provided from router location: object }, @@ -88,43 +92,33 @@ export function ProxyHandler(...redirectFunctions) { // to use the `Lifecycle` widget in further down nested components mixins: [RouteContext], - getInitialState() { - return UserStore.getState(); - }, - componentDidMount() { - UserStore.listen(this.onChange); - UserActions.fetchCurrentUser(); + this.evaluateRedirectFunctions(); }, - componentDidUpdate() { - if(!UserStore.isLoading()) { - const { currentUser } = this.state; - const { query } = this.props.location; + componentWillReceiveProps(nextProps) { + this.evaluateRedirectFunctions(nextProps); + }, - for(let i = 0; i < redirectFunctions.length; i++) { + 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 // stop/avoid the execution of all functions // that follow - if(redirectFunctions[i](currentUser, query)) { + if (redirectFunctions[i](currentUser, query)) { break; } } } }, - componentWillUnmount() { - UserStore.unlisten(this.onChange); - }, - - onChange(state) { - this.setState(state); - }, - render() { return ( - + ); } }); diff --git a/js/components/ascribe_settings/account_settings.js b/js/components/ascribe_settings/account_settings.js index c650358c..c8d4d64a 100644 --- a/js/components/ascribe_settings/account_settings.js +++ b/js/components/ascribe_settings/account_settings.js @@ -26,21 +26,23 @@ let AccountSettings = React.createClass({ whitelabel: React.PropTypes.object.isRequired }, - handleSuccess(){ + handleSuccess() { this.props.loadUser(true); - let notification = new GlobalNotificationModel(getLangText('Settings succesfully updated'), 'success', 5000); + + const notification = new GlobalNotificationModel(getLangText('Settings succesfully updated'), 'success', 5000); GlobalNotificationActions.appendGlobalNotification(notification); }, - getFormDataProfile(){ - return {'email': this.props.currentUser.email}; + getFormDataProfile() { + return { 'email': this.props.currentUser.email }; }, - + render() { - let content = ; + const { currentUser, whitelabel } = this.props; + let content = ; let profile = null; - if (this.props.currentUser.username) { + if (currentUser.username) { content = ( @@ -61,7 +63,7 @@ let AccountSettings = React.createClass({ editable={false}> @@ -70,7 +72,7 @@ let AccountSettings = React.createClass({ ); profile = ( + className="ascribe-property-collapsible-toggle"> + defaultChecked={currentUser.profile.hash_locally}> {' ' + getLangText('Enable hash option, e.g. slow connections or to keep piece private')} @@ -97,9 +98,9 @@ let AccountSettings = React.createClass({ defaultExpanded={true}> {content} - + {profile} diff --git a/js/components/ascribe_settings/contract_settings.js b/js/components/ascribe_settings/contract_settings.js index be723295..37305381 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'; @@ -24,30 +18,29 @@ 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({ + propTypes: { + // Provided from AscribeApp + currentUser: React.PropTypes.object.isRequired, + whitelabel: React.PropTypes.object.isRequired, + + // Provided from router + location: React.PropTypes.object + }, + getInitialState() { - return mergeOptions( - ContractListStore.getState(), - UserStore.getState() - ); + return ContractListStore.getState(); }, componentDidMount() { ContractListStore.listen(this.onChange); - UserStore.listen(this.onChange); - WhitelabelStore.listen(this.onChange); - - WhitelabelActions.fetchWhitelabel(); - UserActions.fetchCurrentUser(); ContractListActions.fetchContractList(true); }, componentWillUnmount() { - WhitelabelStore.unlisten(this.onChange); - UserStore.unlisten(this.onChange); ContractListStore.unlisten(this.onChange); }, @@ -79,6 +72,7 @@ let ContractSettings = React.createClass({ }, render() { + const { currentUser, location, whitelabel } = this.props; const publicContracts = this.getPublicContracts(); const privateContracts = this.getPrivateContracts(); let createPublicContractForm = null; @@ -88,11 +82,11 @@ let ContractSettings = React.createClass({ if (publicContracts.length === 0) { createPublicContractForm = ( + }} + isPublic={true} /> ); } @@ -103,7 +97,7 @@ let ContractSettings = React.createClass({ defaultExpanded={true}> + aclObject={currentUser.acl}>
    {createPublicContractForm} {publicContracts.map((contract, i) => { @@ -115,10 +109,9 @@ let ContractSettings = React.createClass({ buttons={
    - + + aclObject={currentUser.acl}>
    + fileClassToUpload={{ + singular: getLangText('new contract'), + plural: getLangText('new contracts') + }} + isPublic={false} /> {privateContracts.map((contract, i) => { return ( - + - {this.props.children} + whitelabel={whitelabel} /> + {children} diff --git a/js/components/coa_verify_container.js b/js/components/coa_verify_container.js index 6d0af0fa..f0649196 100644 --- a/js/components/coa_verify_container.js +++ b/js/components/coa_verify_container.js @@ -18,6 +18,11 @@ import { setDocumentTitle } from '../utils/dom_utils'; let CoaVerifyContainer = React.createClass({ propTypes: { + // Provided from AscribeApp + currentUser: React.PropTypes.object, + whitelabel: React.PropTypes.object, + + // Provided from router location: React.PropTypes.object }, @@ -27,7 +32,7 @@ let CoaVerifyContainer = React.createClass({ return (
    -
    +
    {getLangText('Verify your Certificate of Authenticity')}
    @@ -37,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-----
    @@ -60,9 +65,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 +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 0e111ce7..046c07a0 100644 --- a/js/components/error_not_found_page.js +++ b/js/components/error_not_found_page.js @@ -1,21 +1,42 @@ 'use strict'; import React from 'react'; +import { History } from 'react-router'; import { getLangText } from '../utils/lang_utils'; let ErrorNotFoundPage = React.createClass({ propTypes: { - message: React.PropTypes.string + message: React.PropTypes.string, + + // Provided from AscribeApp + currentUser: React.PropTypes.object, + whitelabel: React.PropTypes.object, + + // Provided from router + 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 (
    @@ -32,4 +53,4 @@ let ErrorNotFoundPage = React.createClass({ } }); -export default ErrorNotFoundPage; \ No newline at end of file +export default ErrorNotFoundPage; 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 ? ( - ); + ) : null; } }); diff --git a/js/components/header.js b/js/components/header.js index d2ecb76b..ed04a736 100644 --- a/js/components/header.js +++ b/js/components/header.js @@ -14,41 +14,28 @@ 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 PieceListStore from '../stores/piece_list_store'; -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) + currentUser: React.PropTypes.object.isRequired, + routes: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, + whitelabel: React.PropTypes.object.isRequired }, getInitialState() { - return mergeOptions( - PieceListStore.getState(), - WhitelabelStore.getState(), - UserStore.getState() - ); + return PieceListStore.getState(); }, componentDidMount() { @@ -56,35 +43,14 @@ let Header = React.createClass({ // conflicts with routes that may need to wait to load the piece list PieceListStore.listen(this.onChange); - 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. history.listen(this.onRouteChange); - - if (this.state.currentUser && this.state.currentUser.email) { - EventActions.profileDidLoad.defer(this.state.currentUser); - } - }, - - componentWillUpdate(nextProps, nextState) { - const { currentUser: { email: curEmail } = {} } = this.state; - const { currentUser: { email: nextEmail } = {} } = nextState; - - if (nextEmail && curEmail !== nextEmail) { - EventActions.profileDidLoad.defer(nextState.currentUser); - } }, componentWillUnmount() { PieceListStore.unlisten(this.onChange); - UserStore.unlisten(this.onChange); - WhitelabelStore.unlisten(this.onChange); //history.unlisten(this.onRouteChange); }, @@ -93,7 +59,7 @@ let Header = React.createClass({ }, getLogo() { - let { whitelabel } = this.state; + const { whitelabel } = this.props; if (whitelabel.head) { constructHead(whitelabel.head); @@ -117,7 +83,7 @@ let Header = React.createClass({ getPoweredBy() { return (
  • @@ -164,7 +130,9 @@ let Header = React.createClass({ }, render() { - const { currentUser, unfilteredPieceListCount } = this.state; + const { currentUser, routes } = this.props; + const { unfilteredPieceListCount } = this.state; + let account; let signup; let navRoutesLinks; @@ -173,13 +141,13 @@ let Header = React.createClass({ account = ( - + {getLangText('Account Settings')} @@ -189,17 +157,14 @@ let Header = React.createClass({ - + {getLangText('Contract Settings')} - - + + {getLangText('Log out')} @@ -216,21 +181,19 @@ let Header = React.createClass({ navbar right hasPieces={!!unfilteredPieceListCount} - routes={this.props.routes} + routes={routes} userAcl={currentUser.acl} /> ); } else { account = ( - + {getLangText('LOGIN')} ); signup = ( - + {getLangText('SIGNUP')} @@ -246,13 +209,12 @@ let Header = React.createClass({ toggleNavKey={0} fixedTop={true} className="hidden-print"> - + diff --git a/js/components/header_notification.js b/js/components/header_notification.js index 4b375685..fe50e150 100644 --- a/js/components/header_notification.js +++ b/js/components/header_notification.js @@ -11,16 +11,12 @@ import Nav from 'react-bootstrap/lib/Nav'; import NotificationActions from '../actions/notification_actions'; import NotificationStore from '../stores/notification_store'; -import { mergeOptions } from '../utils/general_utils'; import { getLangText } from '../utils/lang_utils'; let HeaderNotifications = React.createClass({ - getInitialState() { - return mergeOptions( - NotificationStore.getState() - ); + return NotificationStore.getState(); }, componentDidMount() { @@ -62,7 +58,7 @@ let HeaderNotifications = React.createClass({ this.refs.dropdownbutton.setDropdownState(false); }, - getPieceNotifications(){ + getPieceNotifications() { if (this.state.pieceListNotifications && this.state.pieceListNotifications.length > 0) { return (
    @@ -87,7 +83,7 @@ let HeaderNotifications = React.createClass({ return null; }, - getEditionNotifications(){ + getEditionNotifications() { if (this.state.editionListNotifications && this.state.editionListNotifications.length > 0) { return (
    @@ -114,7 +110,7 @@ let HeaderNotifications = React.createClass({ render() { if ((this.state.pieceListNotifications && this.state.pieceListNotifications.length > 0) || - (this.state.editionListNotifications && this.state.editionListNotifications.length > 0)){ + (this.state.editionListNotifications && this.state.editionListNotifications.length > 0)) { let numNotifications = 0; if (this.state.pieceListNotifications && this.state.pieceListNotifications.length > 0) { numNotifications += this.state.pieceListNotifications.length; @@ -125,7 +121,8 @@ let HeaderNotifications = React.createClass({ return (
  • ); + } else { + return null; } - return null; }, handleConfirm() { - let contractAgreement = this.state.contractAgreementListNotifications[0].contract_agreement; + const contractAgreement = this.state.contractAgreementListNotifications[0].contract_agreement; OwnershipFetcher .confirmContractAgreement(contractAgreement) .then(this.handleConfirmSuccess); }, handleConfirmSuccess() { - let notification = new GlobalNotificationModel(getLangText('You have accepted the conditions'), 'success', 5000); + const notification = new GlobalNotificationModel(getLangText('You have accepted the conditions'), 'success', 5000); GlobalNotificationActions.appendGlobalNotification(notification); // Flush contract notifications and refetch @@ -123,22 +119,23 @@ let IkonotvContractNotifications = React.createClass({ }, handleDeny() { - let contractAgreement = this.state.contractAgreementListNotifications[0].contract_agreement; + const contractAgreement = this.state.contractAgreementListNotifications[0].contract_agreement; OwnershipFetcher .denyContractAgreement(contractAgreement) .then(this.handleDenySuccess); }, handleDenySuccess() { - let notification = new GlobalNotificationModel(getLangText('You have denied the conditions'), 'success', 5000); + const notification = new GlobalNotificationModel(getLangText('You have denied the conditions'), 'success', 5000); GlobalNotificationActions.appendGlobalNotification(notification); + this.history.push('/collection'); }, getCopyrightAssociationForm(){ - let currentUser = this.state.currentUser; + const { currentUser } = this.props; - if (currentUser && currentUser.profile && !currentUser.profile.copyright_association) { + if (currentUser.profile && !currentUser.profile.copyright_association) { return (

    {getLangText('Are you a member of any copyright societies?')}

    @@ -149,24 +146,26 @@ let IkonotvContractNotifications = React.createClass({
    ); + } else { + return null; } - return null; }, render() { + const { whitelabel } = this.props; + const { contractAgreementListNotifications } = this.state; + setDocumentTitle(getLangText('Contacts notifications')); - if (this.state.contractAgreementListNotifications && - this.state.contractAgreementListNotifications.length > 0) { - - let notifications = this.state.contractAgreementListNotifications[0]; - let blob = notifications.contract_agreement.contract.blob; + if (contractAgreementListNotifications && contractAgreementListNotifications.length) { + const notifications = contractAgreementListNotifications[0]; + const blob = notifications.contract_agreement.contract.blob; return (
    - +
    {getLangText('Contract')}
    @@ -178,7 +177,7 @@ let IkonotvContractNotifications = React.createClass({ - Download PDF version + {getLangText('Download PDF version')}
    @@ -195,8 +194,9 @@ let IkonotvContractNotifications = React.createClass({
    ); + } else { + return null; } - return null; } }); 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 a1280cc8..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 @@ -3,29 +3,27 @@ import React from 'react'; import { History } from 'react-router'; -import PieceActions from '../../../../../../actions/piece_actions'; -import PieceStore from '../../../../../../stores/piece_store'; +import EditionListActions from '../../../../../../actions/edition_list_actions'; -import UserStore from '../../../../../../stores/user_store'; +import GlobalNotificationModel from '../../../../../../models/global_notification_model'; +import GlobalNotificationActions from '../../../../../../actions/global_notification_actions'; import PieceListStore from '../../../../../../stores/piece_list_store'; import PieceListActions from '../../../../../../actions/piece_list_actions'; -import EditionListActions from '../../../../../../actions/edition_list_actions'; +import PieceActions from '../../../../../../actions/piece_actions'; +import PieceStore from '../../../../../../stores/piece_store'; import IkonotvSubmitButton from '../ikonotv_buttons/ikonotv_submit_button'; -import CollapsibleParagraph from '../../../../../../components/ascribe_collapsible/collapsible_paragraph'; - import IkonotvArtistDetailsForm from '../ikonotv_forms/ikonotv_artist_details_form'; import IkonotvArtworkDetailsForm from '../ikonotv_forms/ikonotv_artwork_details_form'; import WalletPieceContainer from '../../ascribe_detail/wallet_piece_container'; -import AscribeSpinner from '../../../../../ascribe_spinner'; +import CollapsibleParagraph from '../../../../../../components/ascribe_collapsible/collapsible_paragraph'; -import GlobalNotificationModel from '../../../../../../models/global_notification_model'; -import GlobalNotificationActions from '../../../../../../actions/global_notification_actions'; +import AscribeSpinner from '../../../../../ascribe_spinner'; import { getLangText } from '../../../../../../utils/lang_utils'; import { setDocumentTitle } from '../../../../../../utils/dom_utils'; @@ -34,6 +32,12 @@ import { mergeOptions } from '../../../../../../utils/general_utils'; let IkonotvPieceContainer = React.createClass({ propTypes: { + // Provided from PrizeApp + currentUser: React.PropTypes.object, + whitelabel: React.PropTypes.object, + + // Provided from router + location: React.PropTypes.object, params: React.PropTypes.object }, @@ -41,15 +45,13 @@ let IkonotvPieceContainer = React.createClass({ getInitialState() { return mergeOptions( - PieceStore.getInitialState(), - UserStore.getState(), - PieceListStore.getState() + PieceListStore.getState(), + PieceStore.getInitialState() ); }, componentDidMount() { PieceStore.listen(this.onChange); - UserStore.listen(this.onChange); PieceListStore.listen(this.onChange); this.loadPiece(); @@ -65,7 +67,6 @@ let IkonotvPieceContainer = React.createClass({ componentWillUnmount() { PieceStore.unlisten(this.onChange); - UserStore.unlisten(this.onChange); PieceListStore.listen(this.onChange); }, @@ -94,6 +95,7 @@ let IkonotvPieceContainer = React.createClass({ }, render() { + const { currentUser } = this.props; const { piece } = this.state; let furtherDetails = ( @@ -127,7 +129,7 @@ let IkonotvPieceContainer = React.createClass({ return ( diff --git a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_landing.js b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_landing.js index 0758d6a9..e1e07dd5 100644 --- a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_landing.js +++ b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_landing.js @@ -6,47 +6,32 @@ import Button from 'react-bootstrap/lib/Button'; import LinkContainer from 'react-router-bootstrap/lib/LinkContainer'; -import UserStore from '../../../../../stores/user_store'; -import UserActions from '../../../../../actions/user_actions'; - import { getLangText } from '../../../../../utils/lang_utils'; import { setDocumentTitle } from '../../../../../utils/dom_utils'; let IkonotvLanding = React.createClass({ propTypes: { + // Provided from PrizeApp + currentUser: React.PropTypes.object.isRequired, + whitelabel: React.PropTypes.object, + + // Provided from router location: React.PropTypes.object }, - getInitialState() { - return UserStore.getState(); - }, - - componentDidMount() { - UserStore.listen(this.onChange); - UserActions.fetchCurrentUser(); - }, - - componentWillUnmount() { - UserStore.unlisten(this.onChange); - }, - - onChange(state) { - this.setState(state); - }, - getEnterButton() { + const { currentUser, location } = this.props; let redirect = '/login'; - if(this.state.currentUser && this.state.currentUser.email) { + if (currentUser.email) { redirect = '/collection'; - } - else if (this.props.location.query && this.props.location.query.redirect) { - redirect = '/' + this.props.location.query.redirect; + } else if (location.query.redirect) { + redirect = '/' + location.query.redirect; } return ( - + 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 eccfd89c..defabec2 100644 --- a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_piece_list.js +++ b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_piece_list.js @@ -4,47 +4,34 @@ import React from 'react'; import PieceList from '../../../../piece_list'; -import UserActions from '../../../../../actions/user_actions'; -import UserStore from '../../../../../stores/user_store'; - import NotificationStore from '../../../../../stores/notification_store'; -import WhitelabelActions from '../../../../../actions/whitelabel_actions'; -import WhitelabelStore from '../../../../../stores/whitelabel_store'; - import IkonotvAccordionListItem from './ikonotv_accordion_list/ikonotv_accordion_list_item'; import { setDocumentTitle } from '../../../../../utils/dom_utils'; -import { mergeOptions } from '../../../../../utils/general_utils'; import { getLangText } from '../../../../../utils/lang_utils'; let IkonotvPieceList = React.createClass({ propTypes: { + // Provided from PrizeApp + currentUser: React.PropTypes.object.isRequired, + whitelabel: React.PropTypes.object.isRequired, + + // Provided from router location: React.PropTypes.object }, getInitialState() { - return mergeOptions( - NotificationStore.getState(), - UserStore.getState(), - WhitelabelStore.getState() - ); + return NotificationStore.getState(); }, componentDidMount() { NotificationStore.listen(this.onChange); - WhitelabelStore.listen(this.onChange); - UserStore.listen(this.onChange); - - WhitelabelActions.fetchWhitelabel(); - UserActions.fetchCurrentUser(); }, componentWillUnmount() { NotificationStore.unlisten(this.onChange); - WhitelabelStore.unlisten(this.onChange); - UserStore.unlisten(this.onChange); }, onChange(state) { @@ -52,11 +39,11 @@ let IkonotvPieceList = React.createClass({ }, shouldRedirect(pieceCount) { - const { contractAgreementListNotifications, - currentUser: { email: userEmail }, + const { currentUser: { email: userEmail }, whitelabel: { user: whitelabelAdminEmail - } } = this.state; + } } = this.props; + const { contractAgreementListNotifications } = this.state; return contractAgreementListNotifications && !contractAgreementListNotifications.length && @@ -70,13 +57,7 @@ let IkonotvPieceList = React.createClass({ return (
    + 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 d2e3ac99..bb46d76d 100644 --- a/js/components/whitelabel/wallet/components/ikonotv/ikonotv_register_piece.js +++ b/js/components/whitelabel/wallet/components/ikonotv/ikonotv_register_piece.js @@ -10,24 +10,18 @@ import Row from 'react-bootstrap/lib/Row'; import PieceListStore from '../../../../../stores/piece_list_store'; import PieceListActions from '../../../../../actions/piece_list_actions'; -import UserStore from '../../../../../stores/user_store'; -import UserActions from '../../../../../actions/user_actions'; - import PieceStore from '../../../../../stores/piece_store'; import PieceActions from '../../../../../actions/piece_actions'; -import WhitelabelActions from '../../../../../actions/whitelabel_actions'; -import WhitelabelStore from '../../../../../stores/whitelabel_store'; - import GlobalNotificationModel from '../../../../../models/global_notification_model'; import GlobalNotificationActions from '../../../../../actions/global_notification_actions'; -import RegisterPieceForm from '../../../../ascribe_forms/form_register_piece'; -import LoanForm from '../../../../ascribe_forms/form_loan'; - import IkonotvArtistDetailsForm from './ikonotv_forms/ikonotv_artist_details_form'; import IkonotvArtworkDetailsForm from './ikonotv_forms/ikonotv_artwork_details_form'; +import RegisterPieceForm from '../../../../ascribe_forms/form_register_piece'; +import LoanForm from '../../../../ascribe_forms/form_loan'; + import SlidesContainer from '../../../../ascribe_slides_container/slides_container'; import ApiUrls from '../../../../../constants/api_urls'; @@ -39,31 +33,31 @@ import { getLangText } from '../../../../../utils/lang_utils'; let IkonotvRegisterPiece = React.createClass({ propTypes: { handleSuccess: React.PropTypes.func, - piece: React.PropTypes.object.isRequired, + + // Provided from PrizeApp + currentUser: React.PropTypes.object.isRequired, + whitelabel: React.PropTypes.object.isRequired, + + // Provided from router location: React.PropTypes.object }, mixins: [History], - getInitialState(){ + getInitialState() { return mergeOptions( - UserStore.getState(), PieceListStore.getState(), PieceStore.getInitialState(), - WhitelabelStore.getState(), { step: 0, pageExitWarning: getLangText("If you leave this form now, your work will not be loaned to Ikono TV.") - }); + } + ); }, componentDidMount() { PieceListStore.listen(this.onChange); - UserStore.listen(this.onChange); PieceStore.listen(this.onChange); - WhitelabelStore.listen(this.onChange); - UserActions.fetchCurrentUser(); - WhitelabelActions.fetchWhitelabel(); const queryParams = this.props.location.query; @@ -81,9 +75,7 @@ let IkonotvRegisterPiece = React.createClass({ componentWillUnmount() { PieceListStore.unlisten(this.onChange); - UserStore.unlisten(this.onChange); PieceStore.unlisten(this.onChange); - WhitelabelStore.listen(this.onChange); }, onChange(state) { @@ -144,8 +136,7 @@ let IkonotvRegisterPiece = React.createClass({ }, canSubmit() { - let currentUser = this.state.currentUser; - let whitelabel = this.state.whitelabel; + const { currentUser, whitelabel } = this.props; return currentUser.acl && currentUser.acl.acl_wallet_submit && whitelabel.user; }, @@ -187,7 +178,8 @@ let IkonotvRegisterPiece = React.createClass({ getSlideLoan() { if (this.canSubmit()) { - const { piece, whitelabel } = this.state; + const { whitelabel } = this.props; + const { piece } = this.state; const today = new Moment(); const endDate = new Moment().add(2, 'years'); @@ -218,7 +210,8 @@ let IkonotvRegisterPiece = React.createClass({ }, render() { - const { pageExitWarning } = this.state; + const { location } = this.props; + const { pageExitWarning, step } = this.state; return (
    0} + {...this.props} + disabled={step > 0} enableLocalHashing={false} - headerMessage={getLangText('Register work')} - submitMessage={getLangText('Register')} - isFineUploaderActive={true} handleSuccess={this.handleRegisterSuccess} - location={this.props.location} /> + headerMessage={getLangText('Register work')} + isFineUploaderActive={true} + submitMessage={getLangText('Register')} />
    diff --git a/js/components/whitelabel/wallet/components/lumenus/lumenus_landing.js b/js/components/whitelabel/wallet/components/lumenus/lumenus_landing.js index eb926f33..093b0ad1 100644 --- a/js/components/whitelabel/wallet/components/lumenus/lumenus_landing.js +++ b/js/components/whitelabel/wallet/components/lumenus/lumenus_landing.js @@ -5,46 +5,31 @@ import React from 'react'; 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 { mergeOptions } from '../../../../../utils/general_utils'; import { getLangText } from '../../../../../utils/lang_utils'; import { setDocumentTitle } from '../../../../../utils/dom_utils'; let LumenusLanding = React.createClass({ + propTypes: { + // Provided from PrizeApp + currentUser: React.PropTypes.object, + whitelabel: React.PropTypes.object.isRequired, - getInitialState() { - return mergeOptions( - WhitelabelStore.getState() - ); + // Provided from router + location: React.PropTypes.object }, componentWillMount() { setDocumentTitle('Lumenus Marketplace'); }, - componentDidMount() { - WhitelabelStore.listen(this.onChange); - WhitelabelActions.fetchWhitelabel(); - }, - - componentWillUnmount() { - WhitelabelStore.unlisten(this.onChange); - }, - - onChange(state) { - this.setState(state); - }, - render() { return (
    - +
    {getLangText('Lumenus Marketplace is powered by') + ' '} diff --git a/js/components/whitelabel/wallet/components/market/market_buttons/market_acl_button_list.js b/js/components/whitelabel/wallet/components/market/market_buttons/market_acl_button_list.js index 4d4f8918..5f029235 100644 --- a/js/components/whitelabel/wallet/components/market/market_buttons/market_acl_button_list.js +++ b/js/components/whitelabel/wallet/components/market/market_buttons/market_acl_button_list.js @@ -9,63 +9,52 @@ import EmailButton from '../../../../../ascribe_buttons/acls/email_button'; import TransferButton from '../../../../../ascribe_buttons/acls/transfer_button'; import UnconsignButton from '../../../../../ascribe_buttons/acls/unconsign_button'; -import UserActions from '../../../../../../actions/user_actions'; -import UserStore from '../../../../../../stores/user_store'; +import { selectFromObject } from '../../../../../../utils/general_utils'; let MarketAclButtonList = React.createClass({ propTypes: { availableAcls: React.PropTypes.object.isRequired, - className: React.PropTypes.string, - pieceOrEditions: React.PropTypes.array, - handleSuccess: React.PropTypes.func, + currentUser: React.PropTypes.object.isRequired, + handleSuccess: React.PropTypes.func.isRequired, + pieceOrEditions: React.PropTypes.array.isRequired, + whitelabel: React.PropTypes.object.isRequired, + children: React.PropTypes.oneOfType([ React.PropTypes.arrayOf(React.PropTypes.element), React.PropTypes.element - ]) - }, - - getInitialState() { - return UserStore.getState(); - }, - - componentDidMount() { - UserStore.listen(this.onChange); - UserActions.fetchCurrentUser.defer(); - }, - - componentWillUnmount() { - UserStore.unlisten(this.onChange); - }, - - onChange(state) { - this.setState(state); + ]), + className: React.PropTypes.string }, render() { - let { availableAcls, className, pieceOrEditions, handleSuccess } = this.props; + const { + availableAcls, + children, + className, + currentUser, + handleSuccess, + pieceOrEditions, + whitelabel } = this.props; + + const buttonProps = selectFromObject(this.props, [ + 'availableAcls', + 'currentUser', + 'handleSuccess', + 'pieceOrEditions' + ]); + return (
    - - - - {this.props.children} + handleSuccess={handleSuccess} + whitelabel={whitelabel} /> + + + + {children}
    ); } 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 c839dea0..b5dd5d50 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 @@ -5,9 +5,6 @@ import classNames from 'classnames'; import EditionActions from '../../../../../../actions/edition_actions'; -import WhitelabelActions from '../../../../../../actions/whitelabel_actions'; -import WhitelabelStore from '../../../../../../stores/whitelabel_store'; - import MarketAdditionalDataForm from '../market_forms/market_additional_data_form'; import AclFormFactory from '../../../../../ascribe_forms/acl_form_factory'; @@ -16,7 +13,6 @@ import ConsignForm from '../../../../../ascribe_forms/form_consign'; import ModalWrapper from '../../../../../ascribe_modal/modal_wrapper'; import AclProxy from '../../../../../acl_proxy'; -import AscribeSpinner from '../../../../../ascribe_spinner'; import ApiUrls from '../../../../../../constants/api_urls'; @@ -26,31 +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, + whitelabel: React.PropTypes.object.isRequired, className: React.PropTypes.string, handleSuccess: React.PropTypes.func }, - 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 { @@ -99,8 +78,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', @@ -119,6 +104,7 @@ let MarketSubmitButton = React.createClass({ {getLangText('CONSIGN TO %s', whitelabelName.toUpperCase())} ); + const consignForm = ( !isUserAdmin && !pieceCount} bulkModalButtonListType={MarketAclButtonList} - customThumbnailPlaceholder={customThumbnailPlaceholder} - filterParams={filterParams} - location={location} /> + filterParams={filterParams} /> ); } }); 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 aa71c207..92a0b1d3 100644 --- a/js/components/whitelabel/wallet/components/market/market_register_piece.js +++ b/js/components/whitelabel/wallet/components/market/market_register_piece.js @@ -6,17 +6,11 @@ import { History } from 'react-router'; import Col from 'react-bootstrap/lib/Col'; import Row from 'react-bootstrap/lib/Row'; -import PieceStore from '../../../../../stores/piece_store'; -import PieceActions from '../../../../../actions/piece_actions'; - import PieceListStore from '../../../../../stores/piece_list_store'; import PieceListActions from '../../../../../actions/piece_list_actions'; -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 PieceStore from '../../../../../stores/piece_store'; +import PieceActions from '../../../../../actions/piece_actions'; import MarketAdditionalDataForm from './market_forms/market_additional_data_form'; @@ -31,6 +25,11 @@ import { mergeOptions } from '../../../../../utils/general_utils'; let MarketRegisterPiece = React.createClass({ propTypes: { + // Provided from PrizeApp + currentUser: React.PropTypes.object, + whitelabel: React.PropTypes.object.isRequired, + + // Provided from router location: React.PropTypes.object }, @@ -40,8 +39,6 @@ let MarketRegisterPiece = React.createClass({ return mergeOptions( PieceListStore.getState(), PieceStore.getInitialState(), - UserStore.getState(), - WhitelabelStore.getState(), { step: 0 }); @@ -50,11 +47,6 @@ let MarketRegisterPiece = React.createClass({ componentDidMount() { PieceListStore.listen(this.onChange); PieceStore.listen(this.onChange); - UserStore.listen(this.onChange); - WhitelabelStore.listen(this.onChange); - - UserActions.fetchCurrentUser(); - WhitelabelActions.fetchWhitelabel(); const queryParams = this.props.location.query; @@ -70,8 +62,6 @@ let MarketRegisterPiece = React.createClass({ componentWillUnmount() { PieceListStore.unlisten(this.onChange); PieceStore.unlisten(this.onChange); - UserStore.unlisten(this.onChange); - WhitelabelStore.unlisten(this.onChange); }, onChange(state) { @@ -111,12 +101,12 @@ let MarketRegisterPiece = React.createClass({ }, render() { - const { location } = this.props; - const { piece, - step, - whitelabel: { - name: whitelabelName = 'Market' - } } = this.state; + const { + location, + whitelabel: { + name: whitelabelName = 'Market' + } } = this.props + const { piece, step } = this.state; setDocumentTitle(getLangText('Register a new piece')); @@ -133,14 +123,14 @@ let MarketRegisterPiece = React.createClass({ 0} enableLocalHashing={false} - headerMessage={getLangText('Consign to %s', whitelabelName)} - submitMessage={getLangText('Proceed to additional details')} - isFineUploaderActive={true} enableSeparateThumbnail={false} handleSuccess={this.handleRegisterSuccess} - location={location}> + headerMessage={getLangText('Consign to %s', whitelabelName)} + isFineUploaderActive={true} + submitMessage={getLangText('Proceed to additional details')}> diff --git a/js/components/whitelabel/wallet/components/polline/polline_landing.js b/js/components/whitelabel/wallet/components/polline/polline_landing.js index 0601975d..c27d56c6 100644 --- a/js/components/whitelabel/wallet/components/polline/polline_landing.js +++ b/js/components/whitelabel/wallet/components/polline/polline_landing.js @@ -13,34 +13,23 @@ import { setDocumentTitle } from '../../../../../utils/dom_utils'; let PollineLanding = React.createClass({ - getInitialState() { - return WhitelabelStore.getState(); + propTypes: { + // Provided from WalletApp + currentUser: React.PropTypes.object, + whitelabel: React.PropTypes.object.isRequired }, componentWillMount() { setDocumentTitle('Polline Marketplace'); }, - componentDidMount() { - WhitelabelStore.listen(this.onChange); - WhitelabelActions.fetchWhitelabel(); - }, - - componentWillUnmount() { - WhitelabelStore.unlisten(this.onChange); - }, - - onChange(state) { - this.setState(state); - }, - render() { return (
    - +
    {getLangText('Polline Art Marketplace is powered by') + ' '} diff --git a/js/components/whitelabel/wallet/wallet_app.js b/js/components/whitelabel/wallet/wallet_app.js index 1740c999..c9927ac8 100644 --- a/js/components/whitelabel/wallet/wallet_app.js +++ b/js/components/whitelabel/wallet/wallet_app.js @@ -1,57 +1,62 @@ '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 AppRouteWrapper from '../../app_route_wrapper'; +import Footer from '../../footer'; +import Header from '../../header'; + import { getSubdomain } from '../../../utils/general_utils'; let WalletApp = React.createClass({ propTypes: { - children: React.PropTypes.oneOfType([ - React.PropTypes.arrayOf(React.PropTypes.element), - React.PropTypes.element - ]), - history: React.PropTypes.object, - routes: React.PropTypes.arrayOf(React.PropTypes.object) + activeRoute: React.PropTypes.object.isRequired, + children: React.PropTypes.element.isRequired, + history: React.PropTypes.object.isRequired, + routes: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, + + // Provided from AppBase + currentUser: React.PropTypes.object, + whitelabel: React.PropTypes.object }, render() { + const { activeRoute, children, currentUser, history, routes, whitelabel } = this.props; + const subdomain = getSubdomain(); + const path = activeRoute && activeRoute.path; + let header = null; - let subdomain = getSubdomain(); - const { history, routes, children } = this.props; - - // 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; - // 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', 'polline']).indexOf(subdomain) > -1) { + && (['cyland', 'ikonotv', 'lumenus', '23vivi', 'polline']).includes(subdomain)) { header = (
    ); } else { - header =
    ; + header = ( +
    + ); } // 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} - - -
    -
    + +
    ); } }); -export default WalletApp; +export default AppBase(WalletApp); diff --git a/js/components/whitelabel/wallet/wallet_routes.js b/js/components/whitelabel/wallet/wallet_routes.js index 963107bd..633aa7f5 100644 --- a/js/components/whitelabel/wallet/wallet_routes.js +++ b/js/components/whitelabel/wallet/wallet_routes.js @@ -50,193 +50,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 /> + + + + ), 'polline': ( diff --git a/js/constants/application_constants.js b/js/constants/application_constants.js index dd27484b..332eee49 100644 --- a/js/constants/application_constants.js +++ b/js/constants/application_constants.js @@ -78,6 +78,8 @@ const constants = { 'IVARO', 'SIAE', 'JASPAR-SPDA', 'AKKA/LAA', 'LATGA-A', 'SOMAAP', 'ARTEGESTION', 'CARIER', 'BONO', 'APSAV', 'SPA', 'GESTOR', 'VISaRTA', 'RAO', 'LITA', 'DALRO', 'VeGaP', 'BUS', 'ProLitteris', 'AGADU', 'AUTORARTE', 'BUBEDRA', 'BBDA', 'BCDA', 'BURIDA', 'ADAVIS', 'BSDA'], + 'locationThreshold': 10, + 'searchThreshold': 500, 'supportedThumbnailFileFormats': [ diff --git a/js/history.js b/js/history.js index 4e8c03c7..3d0ecff5 100644 --- a/js/history.js +++ b/js/history.js @@ -7,8 +7,12 @@ import AppConstants from './constants/application_constants'; // Remove the trailing slash if present -let baseUrl = AppConstants.baseUrl.replace(/\/$/, ''); +const baseUrl = AppConstants.baseUrl.replace(/\/$/, ''); -export default useBasename(useQueries(createBrowserHistory))({ +const history = useBasename(useQueries(createBrowserHistory))({ basename: baseUrl }); + +history.locationQueue = []; + +export default history; 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)} /> diff --git a/js/stores/user_store.js b/js/stores/user_store.js index 7e0e49b0..4c6725e3 100644 --- a/js/stores/user_store.js +++ b/js/stores/user_store.js @@ -1,9 +1,10 @@ 'use strict'; -import { altUser } from '../alt'; +import { alt, altThirdParty } from '../alt'; + +import EventActions from '../actions/event_actions'; import UserActions from '../actions/user_actions'; - import UserSource from '../sources/user_source'; @@ -11,11 +12,15 @@ class UserStore { constructor() { this.currentUser = {}; this.userMeta = { + hasLoaded: false, err: null }; this.bindActions(UserActions); this.registerAsync(UserSource); + this.exportPublicMethods({ + hasLoaded: this.hasLoaded.bind(this) + }); } onFetchCurrentUser(invalidateCache) { @@ -28,8 +33,14 @@ class UserStore { this.preventDefault(); } - onSuccessFetchCurrentUser({ users: [ user = {} ] }) { + onSuccessFetchCurrentUser({ users: [ user = {} ] = [] }) { + this.userMeta.hasLoaded = true; this.userMeta.err = null; + + if (user.email && user.email !== this.currentUser.email) { + EventActions.userDidAuthenticate(user); + } + this.currentUser = user; } @@ -42,14 +53,27 @@ class UserStore { } onSuccessLogoutCurrentUser() { - this.userMeta.err = null; - this.currentUser = {}; + EventActions.userDidLogout(); + + // Reset all stores back to their initial state + // Don't recycle the whitelabel stores since they're not dependent on login + alt.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) { console.logGlobal(err); + this.userMeta.hasLoaded = true; this.userMeta.err = err; } + + hasLoaded() { + return this.userMeta.hasLoaded; + } } -export default altUser.createStore(UserStore, 'UserStore'); +export default alt.createStore(UserStore, 'UserStore'); diff --git a/js/third_party/intercom_handler.js b/js/third_party/intercom_handler.js index 4ab2ff50..fc14ced2 100644 --- a/js/third_party/intercom_handler.js +++ b/js/third_party/intercom_handler.js @@ -12,25 +12,28 @@ class IntercomHandler { this.loaded = false; } - onProfileDidLoad(profile) { + onUserDidAuthenticate(user) { if (this.loaded) { return; } - /* eslint-disable */ - Intercom('boot', { - /* eslint-enable */ + window.Intercom('boot', { app_id: 'oboxh5w1', - email: profile.email, + email: user.email, subdomain: getSubdomain(), widget: { activator: '#IntercomDefaultWidget' - } + } }); console.log('Intercom loaded'); this.loaded = true; } + onUserDidLogout() { + // kill intercom (with fire) + window.Intercom('shutdown'); + this.loaded = false; + } } export default altThirdParty.createStore(IntercomHandler, 'IntercomHandler'); diff --git a/js/third_party/notifications_handler.js b/js/third_party/notifications_handler.js index 6820617e..8a3f478a 100644 --- a/js/third_party/notifications_handler.js +++ b/js/third_party/notifications_handler.js @@ -17,7 +17,7 @@ class NotificationsHandler { this.loaded = false; } - onProfileDidLoad() { + onUserDidAuthenticate() { if (this.loaded) { return; } @@ -27,8 +27,9 @@ class NotificationsHandler { NotificationActions.fetchContractAgreementListNotifications().then( (res) => { if (res.notifications && res.notifications.length > 0) { - this.loaded = true; console.log('Contractagreement notifications loaded'); + this.loaded = true; + history.push('/contract_notifications'); } } @@ -36,6 +37,10 @@ class NotificationsHandler { } this.loaded = true; } + + onUserDidLogout() { + this.loaded = false; + } } export default altThirdParty.createStore(NotificationsHandler, 'NotificationsHandler'); diff --git a/js/third_party/raven_handler.js b/js/third_party/raven_handler.js index 3d6ff315..f294bc95 100644 --- a/js/third_party/raven_handler.js +++ b/js/third_party/raven_handler.js @@ -12,17 +12,22 @@ class RavenHandler { this.loaded = false; } - onProfileDidLoad(profile) { + onUserDidAuthenticate(user) { if (this.loaded) { return; } Raven.setUserContext({ - email: profile.email + email: user.email }); console.log('Raven loaded'); this.loaded = true; } + + onUserDidLogout() { + Raven.setUserContext(); + this.loaded = false; + } } export default altThirdParty.createStore(RavenHandler, 'RavenHandler'); diff --git a/js/utils/general_utils.js b/js/utils/general_utils.js index c245cef6..8980fd93 100644 --- a/js/utils/general_utils.js +++ b/js/utils/general_utils.js @@ -84,6 +84,8 @@ export function formatText() { * Checks a list of objects for key duplicates and returns a boolean */ function _doesObjectListHaveDuplicates(l) { + let mergedList = []; + l = l.map((obj) => { if(!obj) { throw new Error('The object you are trying to merge is null instead of an empty object'); @@ -92,16 +94,16 @@ function _doesObjectListHaveDuplicates(l) { return Object.keys(obj); }); - // Taken from: http://stackoverflow.com/a/10865042 (but even better with rest) + // Taken from: http://stackoverflow.com/a/10865042 // How to flatten an array of arrays in javascript. // If two objects contain the same key, then these two keys // will actually be represented in the merged array - let mergedList = [].concat(...l); + mergedList = mergedList.concat.apply(mergedList, 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; } /** @@ -149,7 +151,7 @@ export function escapeHTML(s) { * Returns a copy of the given object's own and inherited enumerable * properties, omitting any keys that pass the given filter function. */ -function filterObjOnFn(obj, filterFn) { +function applyFilterOnObject(obj, filterFn) { const filteredObj = {}; for (let key in obj) { @@ -162,6 +164,37 @@ function filterObjOnFn(obj, filterFn) { return filteredObj; } +/** + * Abstraction for selectFromObject and omitFromObject + * for DRYness + * @param {boolean} isInclusion True if the filter should be for including the filtered items + * (ie. selecting only them vs omitting only them) + */ +function filterFromObject(obj, filter, { isInclusion = true } = {}) { + if (filter && filter.constructor === Array) { + return applyFilterOnObject(obj, isInclusion ? ((_, key) => filter.indexOf(key) < 0) + : ((_, key) => filter.indexOf(key) >= 0)); + } else if (filter && typeof filter === 'function') { + // Flip the filter fn's return if it's for inclusion + return applyFilterOnObject(obj, isInclusion ? (...args) => !filter(...args) + : filter); + } else { + throw new Error('The given filter is not an array or function. Exclude aborted'); + } +} + +/** + * Similar to lodash's _.pick(), this returns a copy of the given object's + * own and inherited enumerable properties, selecting only the keys in + * the given array or whose value pass the given filter function. + * @param {object} obj Source object + * @param {array|function} filter Array of key names to select or function to invoke per iteration + * @return {object} The new object +*/ +export function selectFromObject(obj, filter) { + return filterFromObject(obj, filter); +} + /** * Similar to lodash's _.omit(), this returns a copy of the given object's * own and inherited enumerable properties, omitting any keys that are @@ -171,15 +204,51 @@ function filterObjOnFn(obj, filterFn) { * @return {object} The new object */ export function omitFromObject(obj, filter) { - if (filter && filter.constructor === Array) { - return filterObjOnFn(obj, (_, key) => { - return filter.indexOf(key) >= 0; - }); - } else if (filter && typeof filter === 'function') { - return filterObjOnFn(obj, filter); - } else { - throw new Error('The given filter is not an array or function. Exclude aborted'); + return filterFromObject(obj, filter, { isInclusion: false }); +} + +/** + * Recursively tests an object against a "match" object to see if the + * object is similar to the "match" object. In other words, this will + * deeply traverse the "match" object's properties and check them + * against the object by using the testFn. + * + * The object is considered a match if all primitive properties in the + * "match" object are found and accepted in the object by the testFn. + * + * @param {object} obj Object to test + * @param {object} match "Match" object to test against + * @param {(function)} testFn Function to use on each property test. + * Return true to accept the match. + * By default, applies strict equality using === + * @return {boolean} True if obj matches the "match" object + */ +export function deepMatchObject(obj, match, testFn) { + if (typeof match !== 'object') { + throw new Error('Your specified match argument was not an object'); } + + if (typeof testFn !== 'function') { + testFn = (objProp, matchProp) => { + return objProp === matchProp; + }; + } + + return Object + .keys(match) + .reduce((result, matchKey) => { + if (!result) { return false; } + + const objProp = obj[matchKey]; + const matchProp = match[matchKey]; + + if (typeof matchProp === 'object') { + return (typeof objProp === 'object') ? deepMatchObject(objProp, matchProp, testFn) + : false; + } else { + return testFn(objProp, matchProp); + } + }, true); } /** diff --git a/package.json b/package.json index 091203b5..f50b951d 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,24 @@ "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 ./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", + "vi-test": "npm run -s vi-test:base", + "vi-test:base": "npm run vi-clean && gemini test -c ./test/gemini/.gemini.yml --reporter html --reporter vflat", + "vi-test:all": "npm run -s vi-test", + "vi-test:main": "npm run -s vi-test:base -- --browser MainDesktop --browser MainMobile", + "vi-test:whitelabel": "GEMINI_BROWSERS='CcDesktop, CcMobile, CylandDesktop, CylandMobile, IkonotvDesktop, IkonotvMobile, LumenusDesktop, LumenusMobile, 23viviDesktop, 23viviMobile' npm run -s vi-test:base", + "vi-test:cc": "npm run -s vi-test:base -- --browser CcDesktop --browser CcMobile", + "vi-test:cyland": "npm run -s vi-test:base -- --browser CylandDesktop --browser CylandMobile", + "vi-test:ikonotv": "npm run -s vi-test:base -- --browser IkonotvDesktop --browser IkonotvMobile", + "vi-test:lumenus": "npm run -s vi-test:base -- --browser LumenusDesktop --browser LumenusMobile", + "vi-test:23vivi": "npm run -s vi-test:base -- --browser 23viviDesktop --browser 23viviMobile" }, "browser": { "fineUploader": "./js/components/ascribe_uploader/vendor/s3.fine-uploader.js" @@ -42,8 +58,10 @@ "chai-as-promised": "^5.1.0", "colors": "^1.1.2", "dotenv": "^1.2.0", + "gemini": "^2.1.0", "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(); + } +}); diff --git a/sass/ascribe_property.scss b/sass/ascribe_property.scss index d214c57e..ee12f413 100644 --- a/sass/ascribe_property.scss +++ b/sass/ascribe_property.scss @@ -3,7 +3,6 @@ $ascribe-red-error: rgb(169, 68, 66); .ascribe-property-wrapper { background-color: white; border-left: 3px solid rgba(0, 0, 0, 0); - padding-bottom: 1em; text-align: center; width: 100%; @@ -37,7 +36,6 @@ $ascribe-red-error: rgb(169, 68, 66); margin-right: 1em; } } - > input, > textarea { @@ -74,9 +72,7 @@ $ascribe-red-error: rgb(169, 68, 66); border-top: 1px solid rgba(0, 0, 0, .05); cursor: pointer; display: inline-block; - padding-left: 1.5em; - padding-right: 1.5em; - padding-top: 1em; + padding: 1em 1.5em; text-align: left; width: 100%; @@ -233,4 +229,4 @@ $ascribe-red-error: rgb(169, 68, 66); > span > span { margin-top: 0; } -} \ No newline at end of file +} 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..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; @@ -90,7 +89,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..e022f34e 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; } } @@ -72,186 +68,3 @@ $sluice--button-color: $sluice--nav-fg-prim-color; padding: 0.7em; } - -.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; +} diff --git a/sass/whitelabel/wallet/23vivi/23vivi_custom_style.scss b/sass/whitelabel/wallet/23vivi/23vivi_custom_style.scss index 303f457a..4b6ff089 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: #2882fa; /** Landing page **/ .route--landing { display: table; + min-height: 100vh; > .container { display: table-cell; @@ -20,8 +21,13 @@ $vivi23--highlight-color: #2882fa; vertical-align: middle; } + .hero { + display: none; + } + .vivi23-landing { font-weight: normal; + padding: 0 15px; text-align: center; } @@ -34,7 +40,6 @@ $vivi23--highlight-color: #2882fa; .vivi23-landing--header-logo { margin-top: 1em; margin-bottom: 2em; - height: 75px; } } @@ -247,10 +252,6 @@ $vivi23--highlight-color: #2882fa; 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 9af18fcf..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; @@ -178,8 +175,13 @@ $cyland--button-sec-color: #515151; vertical-align: middle; } + .hero { + display: none; + } + .cyland-landing { font-weight: normal; + padding: 0 15px; text-align: center; } } 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 4e3d2a9a..1689a1f1 100644 --- a/sass/whitelabel/wallet/index.scss +++ b/sass/whitelabel/wallet/index.scss @@ -6,7 +6,6 @@ .ascribe-wallet-app { border-radius: 0; - min-height: 100vh; padding-top: 70px; padding-bottom: 10px; } diff --git a/test/gemini/.gemini.yml b/test/gemini/.gemini.yml new file mode 100644 index 00000000..0c7b8b77 --- /dev/null +++ b/test/gemini/.gemini.yml @@ -0,0 +1,133 @@ +rootUrl: http://localhost.com:3000/ +sessionsPerBrowser: 1 + +browsers: + MainDesktop: + rootUrl: http://localhost.com:3000/ + screenshotsDir: './screenshots/main-desktop' + windowSize: 1900x1080 + desiredCapabilities: + browserName: phantomjs + + MainMobile: + rootUrl: http://localhost.com:3000/ + screenshotsDir: './screenshots/main-mobile' + windowSize: 600x1056 + desiredCapabilities: + browserName: phantomjs + + CcDesktop: + rootUrl: http://cc.localhost.com:3000/ + screenshotsDir: './screenshots/cc-desktop' + windowSize: 1900x1080 + desiredCapabilities: + browserName: phantomjs + + CcMobile: + rootUrl: http://cc.localhost.com:3000/ + screenshotsDir: './screenshots/cc-mobile' + windowSize: 600x1056 + desiredCapabilities: + browserName: phantomjs + + CylandDesktop: + rootUrl: http://cyland.localhost.com:3000/ + screenshotsDir: './screenshots/cyland-desktop' + windowSize: 1900x1080 + desiredCapabilities: + browserName: phantomjs + + CylandMobile: + rootUrl: http://cyland.localhost.com:3000/ + screenshotsDir: './screenshots/cyland-mobile' + windowSize: 600x1056 + desiredCapabilities: + browserName: phantomjs + + IkonotvDesktop: + rootUrl: http://ikonotv.localhost.com:3000/ + screenshotsDir: './screenshots/ikonotv-desktop' + windowSize: 1900x1080 + desiredCapabilities: + browserName: phantomjs + + IkonotvMobile: + rootUrl: http://ikonotv.localhost.com:3000/ + screenshotsDir: './screenshots/ikonotv-mobile' + windowSize: 600x1056 + desiredCapabilities: + browserName: phantomjs + + LumenusDesktop: + rootUrl: http://lumenus.localhost.com:3000/ + screenshotsDir: './screenshots/lumenus-desktop' + windowSize: 1900x1080 + desiredCapabilities: + browserName: phantomjs + + LumenusMobile: + rootUrl: http://lumenus.localhost.com:3000/ + screenshotsDir: './screenshots/lumenus-mobile' + windowSize: 600x1056 + desiredCapabilities: + browserName: phantomjs + + 23viviDesktop: + rootUrl: http://23vivi.localhost.com:3000/ + screenshotsDir: './screenshots/23vivi-desktop' + windowSize: 1900x1080 + desiredCapabilities: + browserName: phantomjs + + 23viviMobile: + rootUrl: http://23vivi.localhost.com:3000/ + screenshotsDir: './screenshots/23vivi-mobile' + windowSize: 600x1056 + desiredCapabilities: + browserName: phantomjs + +sets: + main: + files: + - tests/main + browsers: + - MainDesktop + - MainMobile + cc: + files: + - tests/whitelabel/shared + browsers: + - CcDesktop + - CcMobile + + cyland: + files: + - tests/whitelabel/shared + - tests/whitelabel/cyland + browsers: + - CylandDesktop + - CylandMobile + + ikonotv: + files: + - tests/whitelabel/shared + - tests/whitelabel/ikonotv + browsers: + - IkonotvDesktop + - IkonotvMobile + + lumenus: + files: + - tests/whitelabel/shared + - tests/whitelabel/lumenus + browsers: + - LumenusDesktop + - LumenusMobile + + 23vivi: + files: + - tests/whitelabel/shared + - tests/whitelabel/23vivi + browsers: + - 23viviDesktop + - 23viviMobile diff --git a/test/gemini/README.md b/test/gemini/README.md new file mode 100644 index 00000000..3521056e --- /dev/null +++ b/test/gemini/README.md @@ -0,0 +1,208 @@ +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 and Writing Tests](#gemini-usage-and-writing-tests) + 1. [PhantomJS](#phantomjs) + 1. [TODO](#todo) + + +Installation +============ + +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 +# 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 + +# Navigate to the binary, ie. /Users/Brett/.nvm/versions/node/v5.4.0/lib/node_modules/phantomjs2/lib/phantom/bin/phantomjs +upx -d phantomjs + +``` + +Finally, [install Gemini globally and locally with npm](https://github.com/gemini-testing/gemini/blob/master/README.md#installation). + + +Running Tests +============= + +Run PhantomJS: + +```bash +npm run vi-phantom +``` + +And then run Gemini tests: + +```bash +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 + +```bash +npm run vi-update + +# Update just the main app for desktop and mobile +npm run vi-update -- --browser MainDesktop --browser MainMobile +``` + + +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). + +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. + +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 `--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/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 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 + 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. + +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, 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 +phantomjs test/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/test/gemini/tests/environment.js b/test/gemini/tests/environment.js new file mode 100644 index 00000000..abc5e436 --- /dev/null +++ b/test/gemini/tests/environment.js @@ -0,0 +1,35 @@ +'use strict'; + +const MAIN_USER = { + email: 'dimi@mailinator.com', + password: '0000000000' +}; +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: ${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}`); +console.log(` Long: ${TIMEOUTS.LONG}`); +console.log(` Super duper extra long: ${TIMEOUTS.SUPER_DUPER_EXTRA_LONG}\n`); +console.log('========================================================\n'); + +module.exports = { + 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 new file mode 100644 index 00000000..28e4b120 --- /dev/null +++ b/test/gemini/tests/main/authenticated.js @@ -0,0 +1,223 @@ +'use strict'; + +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. +*/ +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()` + actions.waitForElementToShow('.ascribe-app', TIMEOUTS.NORMAL); + }); + + // 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', TIMEOUTS.NORMAL); + + 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)', TIMEOUTS.NORMAL); + }); + }); + + 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)', TIMEOUTS.NORMAL); + }) + .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)', TIMEOUTS.NORMAL); + }) + .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)', 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(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', TIMEOUTS.LONG); + }) + + 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', TIMEOUTS.NORMAL); + }); + }); + + 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', TIMEOUTS.NORMAL); + + 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'); + actions.waitForElementToShow('.ascribe-piece-list-toolbar .search-bar .icon-ascribe-search', TIMEOUTS.NORMAL); + }) + + 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)', TIMEOUTS.NORMAL); + }); + }); + + 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)', TIMEOUTS.NORMAL); + }); + }); + }); + + gemini.suite('Register work', (registerSuite) => { + registerSuite + .setUrl('/register_piece') + .before((actions, find) => { + // The editions options are only rendered after the whitelabel is fetched, so + // we have to wait for it here + // We have to check for the sibling checkbox class as the input itself is hidden + actions.waitForElementToShow('.ascribe-form input[name="num_editions-checkbox"] ~ .checkbox', TIMEOUTS.NORMAL); + }) + .capture('register work', (actions, find) => { + // The uploader options are only rendered after the user is fetched, so + // we also have to wait for it here + 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'); + 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[name="num_editions-checkbox"] ~ .checkbox')); + // Wait for transition + 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', TIMEOUTS.NORMAL); + }) + .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', TIMEOUTS.LONG); + }); + }); +}); diff --git a/test/gemini/tests/main/basic.js b/test/gemini/tests/main/basic.js new file mode 100644 index 00000000..fd07af2b --- /dev/null +++ b/test/gemini/tests/main/basic.js @@ -0,0 +1,147 @@ +'use strict'; + +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. +*/ +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()` + actions.waitForElementToShow('.ascribe-app', TIMEOUTS.NORMAL); + }); + + gemini.suite('Header-desktop', (headerSuite) => { + headerSuite + .setCaptureElements('nav.navbar .container') + .skip(/Mobile/) + .capture('desktop header', (actions, find) => { + actions.waitForElementToShow('nav.navbar .container', TIMEOUTS.NORMAL); + }) + .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', TIMEOUTS.NORMAL); + }) + .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', TIMEOUTS.NORMAL); + }) + .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', TIMEOUTS.NORMAL); + }) + .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, 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')); + }); + }); + + gemini.suite('Sign up', (signUpSuite) => { + signUpSuite + .setUrl('/signup') + .capture('sign up', (actions, find) => { + actions.waitForElementToShow('.ascribe-form', TIMEOUTS.NORMAL); + }) + .capture('sign up form filled with focus', (actions, find) => { + 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')); + }); + }); + + gemini.suite('Password reset', (passwordResetSuite) => { + passwordResetSuite + .setUrl('/password_reset') + .capture('password reset', (actions, find) => { + actions.waitForElementToShow('.ascribe-form', TIMEOUTS.NORMAL); + }) + .capture('password reset form filled with focus', (actions, find) => { + actions.sendKeys(find('.ascribe-form input[name="email"]'), MAIN_USER.email); + }) + .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', TIMEOUTS.NORMAL); + }) + .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/test/gemini/tests/main/detail.js b/test/gemini/tests/main/detail.js new file mode 100644 index 00000000..1a51836b --- /dev/null +++ b/test/gemini/tests/main/detail.js @@ -0,0 +1,132 @@ +'use strict'; + +const gemini = require('gemini'); +const environment = require('../environment'); +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. + * 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()` + actions.waitForElementToShow('.ascribe-app', TIMEOUTS.NORMAL); + + // Wait for the social media buttons to appear + 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) => { + basicPieceSuite + .setUrl(pieceUrl) + .capture('basic piece') + + gemini.suite('Shmui', (shmuiSuite) => { + shmuiSuite. + setCaptureElements('.shmui-wrap') + .capture('shmui', (actions, find) => { + actions.click(find('.ascribe-media-player')); + actions.waitForElementToShow('.shmui-wrap:not(.loading)', TIMEOUTS.SUPER_DUPER_EXTRA_LONG); + // 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-app', TIMEOUTS.NORMAL); + }) + .capture('logged in', (actions, find) => { + 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)', TIMEOUTS.NORMAL); + }); + }); + + 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-app', TIMEOUTS.NORMAL); + }) + .capture('logout', (actions, find) => { + 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 new file mode 100644 index 00000000..9d0c397e --- /dev/null +++ b/test/gemini/tests/whitelabel/23vivi/23vivi.js @@ -0,0 +1,28 @@ +'use strict'; + +const gemini = require('gemini'); +const environment = require('../../environment'); +const TIMEOUTS = environment.TIMEOUTS; + +/** + * Suite of tests against 23vivi specific routes + */ +gemini.suite('23vivi', (suite) => { + suite + .setCaptureElements('.ascribe-body') + .before((actions, find) => { + // This will be called before every nested suite begins + actions.waitForElementToShow('.ascribe-app', TIMEOUTS.NORMAL); + }); + + gemini.suite('Landing', (landingSuite) => { + landingSuite + .setUrl('/') + .capture('landing', (actions, find) => { + // Wait for the logo to appear + actions.waitForElementToShow('.vivi23-landing--header-logo', TIMEOUTS.LONG); + }); + }); + + // TODO: add more tests for market specific pages after authentication +}); diff --git a/test/gemini/tests/whitelabel/cyland/cyland.js b/test/gemini/tests/whitelabel/cyland/cyland.js new file mode 100644 index 00000000..9228186a --- /dev/null +++ b/test/gemini/tests/whitelabel/cyland/cyland.js @@ -0,0 +1,29 @@ +'use strict'; + +const gemini = require('gemini'); +const environment = require('../../environment'); +const TIMEOUTS = environment.TIMEOUTS; + +/** + * Suite of tests against Cyland specific routes + */ +gemini.suite('Cyland', (suite) => { + suite + .setCaptureElements('.ascribe-body') + .before((actions, find) => { + // This will be called before every nested suite begins + actions.waitForElementToShow('.ascribe-app', TIMEOUTS.NORMAL); + }); + + 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', TIMEOUTS.LONG); + }); + }); + + // TODO: add more tests for cyland specific pages after authentication +}); diff --git a/test/gemini/tests/whitelabel/ikonotv/ikonotv.js b/test/gemini/tests/whitelabel/ikonotv/ikonotv.js new file mode 100644 index 00000000..81e936e9 --- /dev/null +++ b/test/gemini/tests/whitelabel/ikonotv/ikonotv.js @@ -0,0 +1,95 @@ +'use strict'; + +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 + */ +gemini.suite('Ikonotv', (suite) => { + suite + .setCaptureElements('.ascribe-body') + .before((actions, find) => { + // This will be called before every nested suite begins + actions.waitForElementToShow('.ascribe-app', TIMEOUTS.NORMAL); + }); + + 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', TIMEOUTS.LONG); + }); + }); + + // 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-app') + .before((actions, find) => { + // This will be called before every nested suite begins unless that suite + // also defines a `.before()` + actions.waitForElementToShow('.ascribe-app', TIMEOUTS.NORMAL); + + // Wait for the forms to appear + actions.waitForElementToShow('.ascribe-form', TIMEOUTS.NORMAL); + + // Just use a dumb wait because the logo is set as a background image + actions.wait(TIMEOUTS.SHORT); + }); + + 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, 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')); + }); + }); + + 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]'), 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')); + }); + }); + }); + + // TODO: add more tests for ikonotv specific pages after authentication +}); diff --git a/test/gemini/tests/whitelabel/lumenus/lumenus.js b/test/gemini/tests/whitelabel/lumenus/lumenus.js new file mode 100644 index 00000000..b15f31ee --- /dev/null +++ b/test/gemini/tests/whitelabel/lumenus/lumenus.js @@ -0,0 +1,28 @@ +'use strict'; + +const gemini = require('gemini'); +const environment = require('../../environment'); +const TIMEOUTS = environment.TIMEOUTS; + +/** + * Suite of tests against lumenus specific routes + */ +gemini.suite('Lumenus', (suite) => { + suite + .setCaptureElements('.ascribe-body') + .before((actions, find) => { + // This will be called before every nested suite begins + actions.waitForElementToShow('.ascribe-app', TIMEOUTS.NORMAL); + }); + + gemini.suite('Landing', (landingSuite) => { + landingSuite + .setUrl('/') + .capture('landing', (actions, find) => { + // Wait for the logo to appear + actions.waitForElementToShow('.wp-landing-wrapper img', TIMEOUTS.LONG); + }); + }); + + // TODO: add more tests for market specific pages after authentication +}); diff --git a/test/gemini/tests/whitelabel/shared/whitelabel_basic.js b/test/gemini/tests/whitelabel/shared/whitelabel_basic.js new file mode 100644 index 00000000..0d5ac26b --- /dev/null +++ b/test/gemini/tests/whitelabel/shared/whitelabel_basic.js @@ -0,0 +1,115 @@ +'use strict'; + +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. +*/ +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', TIMEOUTS.NORMAL); + + // 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', 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(TIMEOUTS.SHORT); + }) + .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, 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')); + }); + }); + + gemini.suite('Sign up', (signUpSuite) => { + signUpSuite + .setUrl('/signup') + // See Ikono + .skip(/Ikono/) + .capture('sign up', (actions, find) => { + 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]'), 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')); + }); + }); + + gemini.suite('Password reset', (passwordResetSuite) => { + passwordResetSuite + .setUrl('/password_reset') + .capture('password reset', (actions, find) => { + 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"]'), MAIN_USER.email); + }) + .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', TIMEOUTS.NORMAL); + // 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'); + }); +}); 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