diff --git a/.gitignore b/.gitignore index 3b59ff2f5..5a42acb67 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,12 @@ node_modules -npm-debug.log +npm-debug.log* dist !dist/coral-admin dist/coral-admin/bundle.js +tests/e2e/reports .DS_Store *.iml +dump.rdb .env gaba.cfg .idea/ diff --git a/bin/cli-serve b/bin/cli-serve index 0c32cbe8e..dd682f5ad 100755 --- a/bin/cli-serve +++ b/bin/cli-serve @@ -17,7 +17,8 @@ const util = require('../util'); /** * Get port from environment and store in Express. */ -const port = normalizePort(process.env.TALK_PORT || '3000'); + +const port = normalizePort(process.env.TALK_PORT || (process.env.NODE_ENV === 'test' ? '3011' : '3000')); app.set('port', port); diff --git a/client/coral-embed-stream/src/CommentStream.js b/client/coral-embed-stream/src/CommentStream.js index 40ad72a41..0fda9444d 100644 --- a/client/coral-embed-stream/src/CommentStream.js +++ b/client/coral-embed-stream/src/CommentStream.js @@ -7,6 +7,7 @@ import { Notification, notificationActions, authActions, + configActions } from '../../coral-framework'; import CommentBox from '../../coral-plugin-commentbox/CommentBox'; @@ -21,14 +22,17 @@ import LikeButton from '../../coral-plugin-likes/LikeButton'; import PermalinkButton from '../../coral-plugin-permalinks/PermalinkButton'; import SignInContainer from '../../coral-sign-in/containers/SignInContainer'; import UserBox from '../../coral-sign-in/components/UserBox'; + import {TabBar, Tab, TabContent, Spinner} from '../../coral-ui'; import SettingsContainer from '../../coral-settings/containers/SettingsContainer'; import RestrictedContent from '../../coral-framework/components/RestrictedContent'; import SuspendedAccount from '../../coral-framework/components/SuspendedAccount'; +import CloseCommentsInfo from '../../coral-framework/components/CloseCommentsInfo'; const {addItem, updateItem, postItem, getStream, postAction, deleteAction, appendItemArray} = itemActions; const {addNotification, clearNotification} = notificationActions; const {logout, showSignInDialog} = authActions; +const {updateOpenStatus} = configActions; class CommentStream extends Component { @@ -40,6 +44,7 @@ class CommentStream extends Component { }; this.changeTab = this.changeTab.bind(this); + this.toggleStatus = this.toggleStatus.bind(this); } changeTab (tab) { @@ -48,6 +53,10 @@ class CommentStream extends Component { }); } + toggleStatus () { + this.props.updateStatus(this.props.config.status === 'open' ? 'closed' : 'open'); + } + static propTypes = { items: PropTypes.object.isRequired, addItem: PropTypes.func.isRequired, @@ -56,31 +65,22 @@ class CommentStream extends Component { componentDidMount () { // Set up messaging between embedded Iframe an parent component - // Using recommended Pym init code which violates .eslint standards - const pym = new Pym.Child({polling: 100}); + this.pym = new Pym.Child({polling: 100}); - if (/https?\:\/\/([^?]+)/.test(pym.parentUrl)) { - this.props.getStream(pym.parentUrl); - } else { - this.props.getStream(window.location); - } + const path = /https?\:\/\/([^?#]+)/.exec(this.pym.parentUrl); + + this.props.getStream(path[1] || window.location); + this.path = path; + + this.pym.sendMessage('childReady'); } render () { - if (Object.keys(this.props.items).length === 0) { - // Loading mock asset - this.props.postItem({ - comments: [], - url: 'http://coralproject.net' - }, 'asset', 'assetTest'); - } - - // TODO: Replace teststream id with id from params - const rootItemId = this.props.items.assets && Object.keys(this.props.items.assets)[0]; const rootItem = this.props.items.assets && this.props.items.assets[rootItemId]; const {actions, users, comments} = this.props.items; const {loggedIn, user, showSignInDialog} = this.props.auth; + const {status} = this.props.config; const {activeTab} = this.state; return
@@ -90,135 +90,145 @@ class CommentStream extends Component { Settings + Configure Stream - {loggedIn && } + {loggedIn && } {/* Add to the restricted param a boolean if the user is suspended*/} }> -
- - + { + status === 'open' + ?
+ + +
+ :

Comments are closed for this thread.

+ } {!loggedIn && } -
- { - rootItem.comments && rootItem.comments.map((commentId) => { - const comment = comments[commentId]; - return
-
- - - -
- { + const comment = comments[commentId]; + return
+
+ + + +
+ + +
+
+ + +
+ - -
-
- - -
- - { - comment.children && - comment.children.map((replyId) => { - let reply = this.props.items.comments[replyId]; - return
-
- - - -
- { + let reply = this.props.items.comments[replyId]; + return
+
+ + + +
+ + +
+
+ + +
+ - -
-
- - -
- -
; - }) +
; + }) }
; - }) - } + }) + } + - {!loggedIn && } + + +

{status === 'open' ? 'Close' : 'Open'} Comment Stream

+ + +
: - + }
; } @@ -263,6 +278,7 @@ const mapDispatchToProps = (dispatch) => ({ appendItemArray: (item, property, value, addToFront, itemType) => dispatch(appendItemArray(item, property, value, addToFront, itemType)), handleSignInDialog: () => dispatch(authActions.showSignInDialog()), logout: () => dispatch(logout()), + updateStatus: status => dispatch(updateOpenStatus(status)) }); export default connect(mapStateToProps, mapDispatchToProps)(CommentStream); diff --git a/client/coral-embed-stream/style/default.css b/client/coral-embed-stream/style/default.css index 21e40ce98..59efc3dc8 100644 --- a/client/coral-embed-stream/style/default.css +++ b/client/coral-embed-stream/style/default.css @@ -197,3 +197,42 @@ hr { float: right; margin: 8px; } + +/* Close comments */ + +.close-comments-intro-wrapper { + display: flex; + justify-content: space-between; + align-items: center; +} + +.close-comments-intro-wrapper button { + width: 300px; + margin-left: 20px; +} + +.close-comments-intro-wrapper button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.close-comments-message { + box-sizing: border-box; + width: 100%; + height: 100px; +} + +.close-comments-confirm-wrapper { + float: right; +} + +.close-comments-alert { + background-color: #d65344; + color: white; + font-size: 16px; + padding: 5px; +} + +.close-comments-alert i.material-icons { + font-size: 16px !important; +} diff --git a/client/coral-framework/actions/config.js b/client/coral-framework/actions/config.js new file mode 100644 index 000000000..6dc880434 --- /dev/null +++ b/client/coral-framework/actions/config.js @@ -0,0 +1,18 @@ +import coralApi from '../helpers/response'; +/* Config Actions */ + +/** + * Action name constants + */ + +export const OPEN_COMMENTS = 'OPEN_COMMENTS'; +export const CLOSE_COMMENTS = 'CLOSE_COMMENTS'; +export const ADD_ITEM = 'ADD_ITEM'; + +export const updateOpenStatus = status => (dispatch, getState) => { + const assetId = getState().items.get('assets') + .keySeq() + .toArray()[0]; + return coralApi(`/asset/${assetId}/status?status=${status}`, {method: 'PUT'}) + .then(() => dispatch({type: status === 'open' ? OPEN_COMMENTS : CLOSE_COMMENTS})); +}; diff --git a/client/coral-framework/components/CloseCommentsInfo.js b/client/coral-framework/components/CloseCommentsInfo.js new file mode 100644 index 000000000..627328f9b --- /dev/null +++ b/client/coral-framework/components/CloseCommentsInfo.js @@ -0,0 +1,23 @@ +import React from 'react'; +import {Button} from 'coral-ui'; + +export default ({status, onClick}) => ( + status === 'open' ? ( +
+

+ This comment stream is currently open. By closing this comment stream, + no new comments may be submitted and all previous comments will still + be displayed. +

+ +
+ ) : ( +
+

+ This comment stream is currently closed. By opening this comment stream, + new comments may be submitted and displayed +

+ +
+ ) +); diff --git a/client/coral-framework/index.js b/client/coral-framework/index.js index 7b721badc..9a679d969 100644 --- a/client/coral-framework/index.js +++ b/client/coral-framework/index.js @@ -4,6 +4,7 @@ import * as itemActions from './actions/items'; import I18n from './modules/i18n/i18n'; import * as notificationActions from './actions/notification'; import * as authActions from './actions/auth'; +import * as configActions from './actions/config'; export { Notification, @@ -12,4 +13,5 @@ export { I18n, notificationActions, authActions, + configActions }; diff --git a/client/coral-framework/reducers/config.js b/client/coral-framework/reducers/config.js index 6521d92a3..9620f5b97 100644 --- a/client/coral-framework/reducers/config.js +++ b/client/coral-framework/reducers/config.js @@ -1,19 +1,30 @@ /* @flow */ import {Map} from 'immutable'; -import * as actions from '../actions/items'; +import * as actions from '../actions/config'; const initialState = Map({ - features: Map({}) + features: Map({}), + status: 'open' }); export default (state = initialState, action) => { switch(action.type) { - // Override config if worked case actions.UPDATE_SETTINGS: return action.config; + case actions.OPEN_COMMENTS: + return state.set('status', 'open'); + + case actions.CLOSE_COMMENTS: + return state.set('status', 'closed'); + + case actions.ADD_ITEM: + return action.item_type === 'assets' ? + state.set('status', action.item.status) + : state; + default: return state; } diff --git a/client/coral-plugin-commentbox/CommentBox.js b/client/coral-plugin-commentbox/CommentBox.js index 9ae524652..5f9592cd5 100644 --- a/client/coral-plugin-commentbox/CommentBox.js +++ b/client/coral-plugin-commentbox/CommentBox.js @@ -78,6 +78,7 @@ class CommentBox extends Component { { author && ( diff --git a/client/coral-sign-in/components/SignInContent.js b/client/coral-sign-in/components/SignInContent.js index 395800e6c..de5e77a49 100644 --- a/client/coral-sign-in/components/SignInContent.js +++ b/client/coral-sign-in/components/SignInContent.js @@ -44,7 +44,7 @@ const SignInContent = ({handleChange, formData, ...props}) => (
{ !props.auth.isLoading ? - : @@ -56,7 +56,7 @@ const SignInContent = ({handleChange, formData, ...props}) => ( props.changeView('FORGOT')}>{lang.t('signIn.forgotYourPass')} {lang.t('signIn.needAnAccount')} - props.changeView('SIGNUP')}> + props.changeView('SIGNUP')} id='coralRegister'> {lang.t('signIn.register')} diff --git a/client/coral-sign-in/components/SignUpContent.js b/client/coral-sign-in/components/SignUpContent.js index 38fa554fe..2f9e9afe1 100644 --- a/client/coral-sign-in/components/SignUpContent.js +++ b/client/coral-sign-in/components/SignUpContent.js @@ -69,7 +69,7 @@ const SignUpContent = ({handleChange, formData, ...props}) => ( />
{ !props.auth.isLoading && !props.auth.successSignUp && ( - )} diff --git a/client/coral-sign-in/containers/SignInContainer.js b/client/coral-sign-in/containers/SignInContainer.js index c201a3863..28dc021d2 100644 --- a/client/coral-sign-in/containers/SignInContainer.js +++ b/client/coral-sign-in/containers/SignInContainer.js @@ -132,7 +132,9 @@ class SignInContainer extends Component { const {auth, showSignInDialog, noButton} = this.props; return (
- {!noButton && } + {!noButton && } =7.0.0" diff --git a/pree2e.sh b/pree2e.sh new file mode 100755 index 000000000..8857e5745 --- /dev/null +++ b/pree2e.sh @@ -0,0 +1,2 @@ +node_modules/selenium-standalone/bin/selenium-standalone install +npm start & diff --git a/routes/api/asset/index.js b/routes/api/asset/index.js index 96b83a969..05926bc15 100644 --- a/routes/api/asset/index.js +++ b/routes/api/asset/index.js @@ -96,4 +96,12 @@ router.put('/:asset_id/settings', (req, res, next) => { }); +router.put('/:asset_id/status', (req, res, next) => { + // Update the asset status + Asset + .update({id: req.params.asset_id}, {status: req.query.status}) + .then(() => res.status(204).end()) + .catch((err) => next(err)); +}); + module.exports = router; diff --git a/tests/e2e/globals.js b/tests/e2e/globals.js new file mode 100644 index 000000000..147af2663 --- /dev/null +++ b/tests/e2e/globals.js @@ -0,0 +1,4 @@ +module.exports = { + waitForConditionTimeout: 20000, + baseUrl: 'localhost:3011/' +}; diff --git a/tests/e2e/mocks.js b/tests/e2e/mocks.js new file mode 100644 index 000000000..a4f430cd5 --- /dev/null +++ b/tests/e2e/mocks.js @@ -0,0 +1,25 @@ +const Comments = require('../../models/comment'); +const Users = require('../../models/user'); +const Actions = require('../../models/action'); +const Assets = require('../../models/asset'); +const Settings = require('../../models/setting'); +const globals = require('./globals'); + +/* Create an array of comments */ +module.exports.comments = (comments) => Assets.findOrCreateByUrl(globals.baseUrl) + .then((asset) => { + comments = comments.map((comment) => { + comment.asset_id = asset.id; + return comment; + }); + return Comments.create(comments); + }); + +/* Create an array of users */ +module.exports.users = (users) => Users.createLocalUsers(users); + +/* Create an array of actions */ +module.exports.actions = (actions) => Actions.create(actions); + +/* Update a setting */ +module.exports.settings = (setting) => Settings.init().then(() => Settings.updateSettings(setting)); diff --git a/tests/e2e/pages/embedStream.js b/tests/e2e/pages/embedStream.js new file mode 100644 index 000000000..cc584a656 --- /dev/null +++ b/tests/e2e/pages/embedStream.js @@ -0,0 +1,45 @@ +const fetch = require('node-fetch'); + +const embedCommands = { + ready() { + return this.resizeWindow(1200, 800) + .url(client.globals.baseUrl) + .waitForElementVisible('body', 2000) + .frame('coralStreamIframe'); + }, + setConfig(config, baseUrl) { + return fetch(`${baseUrl}/api/v1/settings`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify(config) + }); + }, + enterComment() { + const comment = 'This is a test comment'; + return this + .waitForElementVisible('@commentBox') + .setValue('@commentBox', comment) + .click('@postButton') + .waitForElementVisible('.comment', 1000); + }, + validateComment(comment) { + return this + .assert.equal(comment, '.comment'); + } +}; + +module.exports = { + commands: [embedCommands], + elements: { + + commentBox: { + selector: '#commentBox' + }, + postButton: { + selector: '#commentBox .coral-plugin-commentbox-button' + } + } +}; diff --git a/tests/e2e/tests/AppTest.js b/tests/e2e/tests/AppTest.js new file mode 100644 index 000000000..36281ed3f --- /dev/null +++ b/tests/e2e/tests/AppTest.js @@ -0,0 +1,13 @@ +module.exports = { + '@tags': ['app'], + 'Base url and Hostname': browser => { + const {baseUrl} = browser.globals; + browser + .url(baseUrl) + .assert.title('Coral Talk') + .waitForElementPresent('body', 1000); + }, + after: client => { + client.end(); + } +}; diff --git a/tests/e2e/tests/EmbedStreamTests.js b/tests/e2e/tests/EmbedStreamTests.js new file mode 100644 index 000000000..65ea9fa76 --- /dev/null +++ b/tests/e2e/tests/EmbedStreamTests.js @@ -0,0 +1,173 @@ +const utils = require('../../utils/e2e-mongoose'); +const mocks = require('../mocks'); + +const mockComment = 'This is a test comment.'; +const mockReply = 'This is a test reply'; +const mockUser = { + email: `${new Date().getTime()}@test.com`, + name: 'Test User', + pw: 'testtesttest' +}; + +module.exports = { + '@tags': ['embed-stream', 'comment', 'premodoff', 'premodon'], + before: () => { + utils.before(); + }, + 'User registers and posts a comment with premod off': client => { + client.perform((client, done) => { + mocks.settings({moderation: 'post'}) + .then(() => { + //Load Page + client.resizeWindow(1200, 800) + .url(client.globals.baseUrl) + .frame('coralStreamIframe') + + //Register and Log In + .waitForElementVisible('#commentBox', 1000) + .waitForElementVisible('#coralSignInButton', 2000) + .click('#coralSignInButton') + .waitForElementVisible('#coralRegister', 1000) + .click('#coralRegister') + .waitForElementVisible('#email', 1000) + .setValue('#email', mockUser.email) + .setValue('#displayName', mockUser.name) + .setValue('#password', mockUser.pw) + .setValue('#confirmPassword', mockUser.pw) + .click('#coralSignUpButton') + .waitForElementVisible('#coralLogInButton', 10000) + .click('#coralLogInButton') + .waitForElementVisible('.coral-plugin-commentbox-button', 4000) + + // Post a comment + .setValue('.coral-plugin-commentbox-textarea', mockComment) + .click('.coral-plugin-commentbox-button') + .waitForElementVisible('.comment', 1000) + + //Verify that it appears + .assert.containsText('.comment', mockComment); + done(); + }) + .catch((err) => { + console.log(err); + done(); + }); + }); + }, + 'User posts a comment with premod on': client => { + client.perform((client, done) => { + mocks.settings({moderation: 'pre'}) + .then(() => { + //Load Page + client.url(client.globals.baseUrl) + .frame('coralStreamIframe'); + + // Post a comment + client.waitForElementVisible('.coral-plugin-commentbox-button', 2000) + .setValue('.coral-plugin-commentbox-textarea', mockComment) + .click('.coral-plugin-commentbox-button') + .waitForElementVisible('#coral-notif', 1000) + + //Verify that it appears + .assert.containsText('#coral-notif', 'moderation team'); + done(); + }) + .catch((err) => { + console.log(err); + done(); + }); + }); + }, + 'User replies to a comment with premod off': client => { + client.perform((client, done) => { + mocks.settings({moderation: 'post'}) + .then(() => { + //Load Page + client.resizeWindow(1200, 800) + .url(client.globals.baseUrl) + .frame('coralStreamIframe'); + + // Post a comment + client.waitForElementVisible('.coral-plugin-commentbox-button', 2000) + .setValue('.coral-plugin-commentbox-textarea', mockComment) + .click('.coral-plugin-commentbox-button') + + // Post a reply + .waitForElementVisible('.coral-plugin-replies-reply-button', 5000) + .click('.coral-plugin-replies-reply-button') + .waitForElementVisible('#replyText') + .setValue('#replyText', mockReply) + .click('.coral-plugin-replies-textarea button') + .waitForElementVisible('.reply', 2000) + + //Verify that it appears + .assert.containsText('.reply', mockReply); + done(); + }) + .catch((err) => { + console.log(err); + done(); + }); + }); + }, + 'User replies to a comment with premod on': client => { + client.perform((client, done) => { + mocks.settings({moderation: 'pre'}) + + // Add a mock user + .then(() => { + return mocks.users([{ + displayName: 'Baby Blue', + email: 'whale@tale.sea', + password: 'krill' + }]); + }) + + // Add a mock preapproved comment by that user + .then((user) => { + return mocks.comments([{ + body: 'Whales are not fish.', + status: 'accepted', + author_id: user.id + }]); + }) + .then(() => { + //Load Page + client.resizeWindow(1200, 800) + .url(client.globals.baseUrl) + .frame('coralStreamIframe'); + + // Post a reply + client.waitForElementVisible('.coral-plugin-replies-reply-button', 5000) + .click('.coral-plugin-replies-reply-button') + .waitForElementVisible('#replyText') + .setValue('#replyText', mockReply) + .click('.coral-plugin-replies-textarea button') + .waitForElementVisible('#coral-notif', 1000) + + //Verify that it appears + .assert.containsText('#coral-notif', 'moderation team'); + done(); + }) + .catch((err) => { + console.log(err); + done(); + }); + }); + }, + 'Total comment count premod on': client => { + client.perform((client, done) => { + client.url(client.globals.baseUrl) + .frame('coralStreamIframe'); + + // Verify that comment count is correct + client.waitForElementVisible('.coral-plugin-comment-count-text', 2000) + .assert.containsText('.coral-plugin-comment-count-text', '1 Comment'); + done(); + }); + }, + after: client => { + utils.after(); + client.end(); + } +}; diff --git a/tests/utils/e2e-mongoose.js b/tests/utils/e2e-mongoose.js new file mode 100644 index 000000000..1bb7e695b --- /dev/null +++ b/tests/utils/e2e-mongoose.js @@ -0,0 +1,34 @@ +const mongoose = require('../../mongoose'); + +// Ensure the NODE_ENV is set to 'test', +// this is helpful when you would like to change behavior when testing. +function clearDB() { + // console.log('Clearing DB', mongoose.connection); + for (let i in mongoose.connection.collections) { + // console.log('Clearing', i); + mongoose.connection.collections[i].remove(function() {}); + } +} + +module.exports = { + before: () => { + clearDB(); + }, + beforeEach: () => { + if (mongoose.connection.readyState === 0) { + mongoose.on('open', function() { + if (err) { + throw err; + } + + return clearDB(); + }); + } else { + return clearDB(); + } + }, + after: () => { + clearDB(); + mongoose.disconnect(); + } +}; diff --git a/views/article.ejs b/views/article.ejs index 6fd420f82..f4d7ef910 100644 --- a/views/article.ejs +++ b/views/article.ejs @@ -15,6 +15,7 @@ width:500px; } + <%= title %>
@@ -35,7 +36,7 @@