diff --git a/bin/cli b/bin/cli index c5c61fbf6..002288648 100755 --- a/bin/cli +++ b/bin/cli @@ -7,6 +7,7 @@ const program = require('commander'); const pkg = require('../package.json'); const dotenv = require('dotenv'); +const util = require('../util'); //============================================================================== // Setting up the program command line arguments. @@ -15,6 +16,7 @@ const dotenv = require('dotenv'); program .version(pkg.version) .option('-c, --config [path]', 'Specify the configuration file to load') + .option('--pid [path]', 'Specify a path to output the current PID to') .parse(process.argv); if (program.config) { @@ -27,6 +29,11 @@ if (program.config) { } } +// If the `--pid` flag is used, put the current pid there. +if (program.pid) { + util.pid(program.pid); +} + // Perform rewrites to the runtime environment variables based on the contents // of the process.env.REWRITE_ENV if it exists. This is done here as it is the // entrypoint for the entire application. @@ -43,4 +50,15 @@ program // If there is no command listed, output help. if (!process.argv.slice(2).length) { program.outputHelp(); + return; } + +// The ensures that the child process that is created here is always cleaned up +// properly when the parent process dies. +util.onshutdown([ + (signal) => { + if ((program.runningCommand.killed === false) && (program.runningCommand.exitCode === null)) { + program.runningCommand.kill(signal); + } + } +]); diff --git a/circle.yml b/circle.yml index 160bc7bb9..c511715a5 100644 --- a/circle.yml +++ b/circle.yml @@ -21,6 +21,8 @@ test: override: # Run the tests using the junit reporter. - MOCHA_FILE=$CIRCLE_TEST_REPORTS/junit/test-results.xml NPM_PACKAGE_CONFIG_MOCHA_REPORTER=mocha-junit-reporter npm run test + # Run the e2e test suite + - E2E_REPORT_PATH=$CIRCLE_TEST_REPORTS/e2e npm run e2e deployment: release: diff --git a/client/coral-admin/src/components/ModerationList.js b/client/coral-admin/src/components/ModerationList.js index 8ce3ec4a4..c1df40d31 100644 --- a/client/coral-admin/src/components/ModerationList.js +++ b/client/coral-admin/src/components/ModerationList.js @@ -8,10 +8,10 @@ import SuspendUserModal from './SuspendUserModal'; // Each action has different meaning and configuration const menuOptionsMap = { - 'reject': {status: 'rejected', icon: 'close', key: 'r'}, - 'approve': {status: 'accepted', icon: 'done', key: 't'}, - 'flag': {status: 'flagged', icon: 'flag', filter: 'Untouched'}, - 'ban': {status: 'banned', icon: 'not interested'} + 'reject': {status: 'REJECTED', icon: 'close', key: 'r'}, + 'approve': {status: 'ACCEPTED', icon: 'done', key: 't'}, + 'flag': {status: 'FLAGGED', icon: 'flag', filter: 'Untouched'}, + 'ban': {status: 'BANNED', icon: 'not interested'} }; // Renders a comment list and allow performing actions @@ -135,9 +135,9 @@ export default class ModerationList extends React.Component { } else if (action.item_type === 'USERS') { // If a user bio or name is rejected, bring up a dialog before suspending them. - if (menuOption === 'rejected') { + if (menuOption === 'REJECTED') { this.setState({suspendUserModal: action}); - } else if (menuOption === 'accepted') { + } else if (menuOption === 'ACCEPTED') { this.props.userStatusUpdate('ACTIVE', action.item_id); } } diff --git a/client/coral-embed-stream/src/Comment.js b/client/coral-embed-stream/src/Comment.js index 93c89ffb0..529420941 100644 --- a/client/coral-embed-stream/src/Comment.js +++ b/client/coral-embed-stream/src/Comment.js @@ -87,7 +87,7 @@ class Comment extends React.Component { return (

diff --git a/client/coral-framework/graphql/fragments/commentView.graphql b/client/coral-framework/graphql/fragments/commentView.graphql index b54c00e67..fb8051155 100644 --- a/client/coral-framework/graphql/fragments/commentView.graphql +++ b/client/coral-framework/graphql/fragments/commentView.graphql @@ -2,6 +2,7 @@ fragment commentView on Comment { id body created_at + status user { id name: displayName diff --git a/client/coral-plugin-commentbox/CommentBox.js b/client/coral-plugin-commentbox/CommentBox.js index 258681a08..41c8f75fb 100644 --- a/client/coral-plugin-commentbox/CommentBox.js +++ b/client/coral-plugin-commentbox/CommentBox.js @@ -75,8 +75,6 @@ class CommentBox extends Component { } else if (postedComment.status === 'PREMOD') { addNotification('success', lang.t('comment-post-notif-premod')); } else { - - // appendItemArray(parent_id || id, related, commentId, !parent_id, parent_type); addNotification('success', 'Your comment has been posted.'); } diff --git a/client/coral-plugin-commentcontent/CommentContent.js b/client/coral-plugin-commentcontent/CommentContent.js index 542776c63..2dab237dd 100644 --- a/client/coral-plugin-commentcontent/CommentContent.js +++ b/client/coral-plugin-commentcontent/CommentContent.js @@ -1,5 +1,5 @@ import React from 'react'; -const name = 'coral-plugin-replies'; +const name = 'coral-plugin-content'; const Content = ({body, styles}) => { const textbreaks = body.split('\n'); diff --git a/nightwatch.conf.js b/nightwatch.conf.js index fb6c5f9c7..2e0cc5020 100644 --- a/nightwatch.conf.js +++ b/nightwatch.conf.js @@ -1,16 +1,21 @@ require('babel-core/register'); +let E2E_REPORT_PATH = './test/e2e/reports'; +if (process.env.E2E_REPORT_PATH && process.env.E2E_REPORT_PATH.length > 0) { + E2E_REPORT_PATH = process.env.E2E_REPORT_PATH; +} + module.exports = { - 'src_folders': './tests/e2e/tests', - 'output_folder': './tests/e2e/reports', - 'page_objects_path': './tests/e2e/pages', - 'globals_path': './tests/e2e/globals', + 'src_folders': './test/e2e/tests', + 'output_folder': E2E_REPORT_PATH, + 'page_objects_path': './test/e2e/pages', + 'globals_path': './test/e2e/globals', 'custom_commands_path' : '', 'custom_assertions_path' : '', 'selenium': { 'start_process': true, 'server_path': 'node_modules/selenium-standalone/.selenium/selenium-server/2.53.1-server.jar', - 'log_path': './tests/e2e/reports', + 'log_path': E2E_REPORT_PATH, 'host': '127.0.0.1', 'port': 6666, 'cli_args': { @@ -36,7 +41,7 @@ module.exports = { 'enabled': true, 'on_failure': true, 'on_error': true, - 'path': './tests/e2e/reports' + 'path': E2E_REPORT_PATH }, 'exclude': [ './tests/e2e/tests/EmbedStreamTests.js' @@ -47,7 +52,3 @@ module.exports = { } } }; - -// "chromeOptions" : { -// "args" : ["start-fullscreen"] -// } diff --git a/package.json b/package.json index 59b31eb69..641d85757 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "test-cover": "TEST_MODE=unit NODE_ENV=test istanbul cover _mocha --report text --check-coverage -- -R spec", "pree2e": "NODE_ENV=test scripts/pree2e.sh", "e2e": "NODE_ENV=test nightwatch", + "poste2e": "NODE_ENV=test scripts/poste2e.sh", "embed-start": "NODE_ENV=development npm run build && ./bin/cli serve --jobs", "postinstall": "npm run build" }, @@ -153,7 +154,7 @@ "redux-mock-store": "^1.2.1", "redux-thunk": "^2.1.0", "regenerator": "^0.8.46", - "selenium-standalone": "latest", + "selenium-standalone": "^5.11.2", "style-loader": "^0.13.1", "supertest": "^2.0.1", "timeago.js": "^2.0.3", diff --git a/scripts/poste2e.sh b/scripts/poste2e.sh new file mode 100755 index 000000000..1a8279da1 --- /dev/null +++ b/scripts/poste2e.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# If there is a PID file from the e2e tests... +if [ -e /tmp/talk-e2e.pid ] +then + + # Then kill the running talk server. + kill $(cat /tmp/talk-e2e.pid) + +fi diff --git a/scripts/pree2e.sh b/scripts/pree2e.sh index 0487a9187..6210c09f0 100755 --- a/scripts/pree2e.sh +++ b/scripts/pree2e.sh @@ -1,15 +1,19 @@ #!/bin/bash # install selenium -selenium-standalone install +selenium-standalone install --config=./selenium.config.js + +#Init settings +./bin/cli settings init # Creating Admin Test User -./bin/cli-users create --flag_mode --email "admin@test.com" --password "test" --name "Admin Test User" --role "admin" +./bin/cli users create --flag_mode --email "admin@test.com" --password "testtest" --name "AdminTestUser" --role "ADMIN" # Creating Moderator Test User -./bin/cli-users create --flag_mode --email "moderator@test.com" --password "test" --name "Moderator Test User" --role "moderator" +./bin/cli users create --flag_mode --email "moderator@test.com" --password "testtest" --name "ModeratorTestUser" --role "MODERATOR" # Creating Commenter Test User -./bin/cli-users create --flag_mode --email "commenter@test.com" --password "test" --name "commenter@test.com" +./bin/cli users create --flag_mode --email "commenter@test.com" --password "testtest" --name "CommenterTestUser" -npm start & +# Start the server and write the PID to a file to be killed later. +./bin/cli --pid /tmp/talk-e2e.pid serve --jobs & diff --git a/selenium.config.js b/selenium.config.js new file mode 100644 index 000000000..b43c31ef0 --- /dev/null +++ b/selenium.config.js @@ -0,0 +1,9 @@ +module.exports = { + drivers: { + chrome: { + version: '2.25', + arch: process.arch, + baseURL: 'https://chromedriver.storage.googleapis.com' + }, + }, +}; diff --git a/test/e2e/globals.js b/test/e2e/globals.js index 22ad3cb4b..0f73e1d4a 100644 --- a/test/e2e/globals.js +++ b/test/e2e/globals.js @@ -4,15 +4,15 @@ module.exports = { users: { admin: { email: 'admin@test.com', - pass: 'test' + pass: 'testtest' }, moderator: { email: 'moderator@test.com', - pass: 'test' + pass: 'testtest' }, commenter: { email: 'commenter@test.com', - pass: 'test' + pass: 'testtest' } }, }; diff --git a/test/e2e/mocks.js b/test/e2e/mocks.js index a4f430cd5..07693651d 100644 --- a/test/e2e/mocks.js +++ b/test/e2e/mocks.js @@ -1,8 +1,8 @@ -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 Comments = require('../../services/comments'); +const Users = require('../../services/users'); +const Actions = require('../../services/actions'); +const Assets = require('../../services/assets'); +const Settings = require('../../services/settings'); const globals = require('./globals'); /* Create an array of comments */ @@ -22,4 +22,5 @@ module.exports.users = (users) => Users.createLocalUsers(users); module.exports.actions = (actions) => Actions.create(actions); /* Update a setting */ -module.exports.settings = (setting) => Settings.init().then(() => Settings.updateSettings(setting)); +module.exports.settings = (setting) => Settings.init().then(() => + Settings.update(setting)); diff --git a/test/e2e/pages/adminPage.js b/test/e2e/pages/adminPage.js index e359715ac..3cc00d5fc 100644 --- a/test/e2e/pages/adminPage.js +++ b/test/e2e/pages/adminPage.js @@ -10,7 +10,8 @@ const embedStreamCommands = { return this .waitForElementVisible('@moderationList') .waitForElementVisible('@approveButton') - .click('@approveButton'); + .click('@approveButton') + .waitForElementNotPresent('@approveButton'); } }; diff --git a/test/e2e/pages/embedStreamPage.js b/test/e2e/pages/embedStreamPage.js index 164449ff0..bdf2f9440 100644 --- a/test/e2e/pages/embedStreamPage.js +++ b/test/e2e/pages/embedStreamPage.js @@ -127,10 +127,10 @@ module.exports = { selector: '.comment .coral-plugin-flags-popup' }, flagCommentOption: { - selector: '.comment .coral-plugin-flags-popup .coral-plugin-flags-popup-radio-label[for="comments"]' + selector: '.comment .coral-plugin-flags-popup .coral-plugin-flags-popup-radio-label[for="COMMENTS"]' }, flagUsernameOption: { - selector: '.comment .coral-plugin-flags-popup .coral-plugin-flags-popup-radio-label[for="user"]' + selector: '.comment .coral-plugin-flags-popup .coral-plugin-flags-popup-radio-label[for="USERS"]' }, flagOtherOption: { selector: '.comment .coral-plugin-flags-popup .coral-plugin-flags-popup-radio-label[for="other"]' diff --git a/test/e2e/tests/01_EmbedStreamTest.js b/test/e2e/tests/01_EmbedStreamTest.js index 35f4171c7..87e94923e 100644 --- a/test/e2e/tests/01_EmbedStreamTest.js +++ b/test/e2e/tests/01_EmbedStreamTest.js @@ -1,10 +1,18 @@ +const mocks = require('../mocks'); + module.exports = { '@tags': ['embedStream'], before: client => { - const embedStreamPage = client.page.embedStreamPage(); - embedStreamPage - .navigate() - .ready(); + client.perform((client, done) => { + mocks.settings({moderation: 'PRE'}) + .then(() => { + const embedStreamPage = client.page.embedStreamPage(); + embedStreamPage + .navigate() + .ready(); + done(); + }); + }); }, 'Login as commenter': client => { const embedStreamPage = client.page.embedStreamPage(); diff --git a/test/e2e/tests/EmbedStreamTests.js b/test/e2e/tests/EmbedStreamTests.js index 70b4c6ad0..7343c3d0a 100644 --- a/test/e2e/tests/EmbedStreamTests.js +++ b/test/e2e/tests/EmbedStreamTests.js @@ -1,12 +1,12 @@ const mongoose = require('../../helpers/mongoose'); const mocks = require('../mocks'); -const mockComment = 'This is a test comment.'; +const mockComment = 'I read the comments'; const mockReply = 'This is a test reply'; const mockUser = { email: `${new Date().getTime()}@test.com`, - name: 'Test User', - pw: 'testtesttest' + name: 'testuser', + pw: 'testtest' }; module.exports = { @@ -29,7 +29,6 @@ module.exports = { .frame('coralStreamIframe') // Register and Log In - .waitForElementVisible('#commentBox', 1000) .waitForElementVisible('#coralSignInButton', 2000) .click('#coralSignInButton') .waitForElementVisible('#coralRegister', 1000) @@ -47,10 +46,10 @@ module.exports = { // Post a comment .setValue('.coral-plugin-commentbox-textarea', mockComment) .click('.coral-plugin-commentbox-button') - .waitForElementVisible('.comment', 1000) + .waitForElementVisible('.coral-plugin-content-text', 1000) // Verify that it appears - .assert.containsText('.comment', mockComment); + .assert.containsText('.coral-plugin-content-text', mockComment); done(); }) .catch((err) => { @@ -104,8 +103,8 @@ module.exports = { .click('.coral-plugin-replies-reply-button') .waitForElementVisible('#replyText') .setValue('#replyText', mockReply) - .click('.coral-plugin-replies-textarea button') - .waitForElementVisible('.reply', 2000) + .click('.coral-plugin-replies-textarea .coral-plugin-commentbox-button') + .waitForElementVisible('.reply', 20000) // Verify that it appears .assert.containsText('.reply', mockReply); @@ -122,22 +121,18 @@ module.exports = { mocks.settings({moderation: 'PRE'}) // Add a mock user - .then(() => { - return mocks.users([{ - displayName: 'Baby Blue', - email: 'whale@tale.sea', - password: 'krill' - }]); - }) + .then(() => 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((user) => mocks.comments([{ + body: 'Whales are not fish.', + status: 'accepted', + author_id: user.id + }])) .then(() => { // Load Page @@ -170,7 +165,7 @@ module.exports = { // Verify that comment count is correct client.waitForElementVisible('.coral-plugin-comment-count-text', 2000) - .assert.containsText('.coral-plugin-comment-count-text', '1 Comment'); + .assert.containsText('.coral-plugin-comment-count-text', '4 Comments'); done(); }); }, diff --git a/test/e2e/tests/Visitor/SignUpTest.js b/test/e2e/tests/Visitor/SignUpTest.js index 9399b47ca..d19ca1d8c 100644 --- a/test/e2e/tests/Visitor/SignUpTest.js +++ b/test/e2e/tests/Visitor/SignUpTest.js @@ -1,5 +1,3 @@ -const uuid = require('uuid'); - module.exports = { '@tags': ['signup', 'visitor'], before: client => { @@ -14,8 +12,8 @@ module.exports = { embedStreamPage .signUp({ - email: `visitor_${uuid.v4()}@test.com`, - displayName: 'Visitor', + email: `visitor_${Date.now()}@test.com`, + displayName: `visitor${Date.now()}`, pass: 'testtest' }); }, diff --git a/util.js b/util.js index b081078e7..585cfdcb7 100644 --- a/util.js +++ b/util.js @@ -1,3 +1,6 @@ +const debug = require('debug')('talk:util'); +const fs = require('fs'); + const util = module.exports = {}; /** @@ -11,10 +14,16 @@ util.toshutdown = []; * Calls all the shutdown functions and then ends the process. * @param {Number} [defaultCode=0] default return code upon sucesfull shutdown. */ -util.shutdown = (defaultCode = 0) => { +util.shutdown = (defaultCode = 0, signal = null) => { + + if (signal) { + debug(`Reached ${signal} signal`); + } + Promise - .all(util.toshutdown.map((func) => func ? func() : null).filter((func) => func)) + .all(util.toshutdown.map((func) => func ? func(signal) : null).filter((func) => func)) .then(() => { + debug('Shutdown complete, now exiting'); process.exit(defaultCode); }) .catch((err) => { @@ -32,13 +41,52 @@ util.shutdown = (defaultCode = 0) => { */ util.onshutdown = (jobs) => { + debug(`${jobs.length} jobs registered`); + // Add the new jobs to shutdown to the object reference. util.toshutdown = util.toshutdown.concat(jobs); }; +/** + * Register a PID file to be maintained for the lifespan of the process. + * @param {String} path path to the PID file to create + */ +util.pid = (path) => { + if (!/\//.test(path)) { + if (!/\.pid/.test(path)) { + path += '.pid'; + } + path = `/tmp/${path}`; + } + + const pid = `${process.pid.toString()}\n`; + + fs.writeFile(path, pid, (err) => { + if (err) { + console.error(`Can't write PID file: ${err}`); + throw err; + } + + // Add the cleanup for the fs onto the shutdown. + util.onshutdown([ + () => new Promise((resolve, reject) => { + + // Remove the pid file. + fs.unlink(path, (err) => { + if (err) { + return reject(err); + } + + return resolve(); + }); + }) + ]); + }); +}; + // Attach to the SIGTERM + SIGINT handles to ensure a clean shutdown in the // event that we have an external event. SIGUSR2 is called when the app is asked // to be 'killed', same procedure here. -process.on('SIGTERM', () => util.shutdown()); -process.on('SIGINT', () => util.shutdown()); -process.once('SIGUSR2', () => util.shutdown()); +process.on('SIGTERM', () => util.shutdown(0, 'SIGTERM')); +process.on('SIGINT', () => util.shutdown(0, 'SIGINT')); +process.once('SIGUSR2', () => util.shutdown(0, 'SIGUSR2'));