Compare commits

...
189 Commits
Author SHA1 Message Date
Kim Gardner 2d43eef508 Merge pull request #1167 from coralproject/query-status-history
Fix SetCommentStatus mutation on admin side
2017-11-16 19:05:23 +00:00
Kiwi fa22e45a4d Merge branch 'master' into query-status-history 2017-11-16 19:53:23 +01:00
Chi Vinh Le abdf5335da Add status_history to query 2017-11-16 19:52:47 +01:00
Wyatt Johnson c24454e234 Merge pull request #1165 from alleyinteractive/expose-recaptcha
Export CheckRecaptcha, so that it can be used in other services.
2017-11-16 11:36:25 -07:00
Wyatt Johnson 97e2e61a4b Merge branch 'master' into expose-recaptcha 2017-11-16 10:19:18 -07:00
Kim Gardner 57ced3c91f Merge pull request #1155 from coralproject/bump-version-3-8-0
Bump version 3.8.0
2017-11-16 16:57:36 +00:00
Max Eddy c664438ade Export CheckRecaptcha, so that it can be used in other services. 2017-11-16 09:09:16 -05:00
Kim Gardner a5b5043a0d Merge branch 'master' into bump-version-3-8-0 2017-11-16 11:47:34 +00:00
Belén Curcio 03a0d904d2 Merge pull request #1164 from coralproject/correct-proptype
Correct proptype
2017-11-15 16:45:30 -03:00
Chi Vinh Le 315d3babde Correct prop types 2 2017-11-15 20:03:41 +01:00
Chi Vinh Le 4d6055b9b8 Correct prop type 2017-11-15 19:53:58 +01:00
Kim Gardner 77c8d966ab Merge pull request #1162 from coralproject/slot-classnames
Slot Classnames
2017-11-15 18:20:21 +00:00
Kim Gardner cabadf467f Merge branch 'master' into slot-classnames 2017-11-15 18:09:11 +00:00
Kim Gardner 0807acc98f Merge pull request #1159 from coralproject/more-undo
Enhanced undo
2017-11-15 18:09:04 +00:00
Belén Curcio 3c5b56c599 Merge branch 'master' into more-undo 2017-11-15 15:00:48 -03:00
Belen Curcio 899bba7327 Adding Proptypes and classNames for the Slots 🎉 2017-11-15 14:50:33 -03:00
Belen Curcio 41dd3eb180 Merge branch 'master' of github.com:coralproject/talk 2017-11-15 14:40:22 -03:00
Kim Gardner 81d90cb138 Merge pull request #1161 from coralproject/break-word-2
Support long words for my and featured comments
2017-11-15 12:46:49 +00:00
Chi Vinh Le 2882dfbadb Fix flexbox overflowing 2017-11-15 13:34:29 +01:00
Chi Vinh Le f2b9db9dbc Support long words for my and featured comments 2017-11-15 13:07:33 +01:00
Kim Gardner c563a13e7c Merge pull request #1160 from coralproject/break-word
Break long words in comment
2017-11-14 16:31:55 +00:00
Belen Curcio 5976595871 Merge branch 'master' of github.com:coralproject/talk 2017-11-14 13:03:10 -03:00
Chi Vinh Le 8a4a7d3589 Break long words in comment 2017-11-14 15:37:26 +01:00
Chi Vinh Le 5073e60df5 Translate 2017-11-14 14:05:30 +01:00
Chi Vinh Le 5a71db2568 Undo to previous status + Live updates 2017-11-14 13:58:18 +01:00
Kiwi ac0acea22b Merge pull request #1149 from devonJS/undo-reject
undo reject comment on stream
2017-11-14 12:49:48 +01:00
Belen Curcio 6198c9e730 Merge branch 'master' of github.com:coralproject/talk 2017-11-13 12:34:44 -03:00
Kiwi 9ac06281e9 Merge branch 'master' into undo-reject 2017-11-13 14:00:21 +01:00
Kim Gardner 78e5cb7aa6 Merge branch 'master' into bump-version-3-8-0 2017-11-13 12:49:47 +00:00
Kiwi 882bf57bbc Merge pull request #1152 from coralproject/user-status
More e2e tests - User Status
2017-11-10 21:16:59 +01:00
Chi Vinh Le 789fb3dfce Disable bs on this branch 2017-11-10 20:59:01 +01:00
Belen Curcio 819b1c597c refactor: working tests 2017-11-10 13:04:46 -03:00
Belen Curcio 38148ad51c Adding post animation test 2017-11-10 12:09:55 -03:00
Belen Curcio 8f6ff46244 passing suspendUserDialog to the adminPage scope 2017-11-10 11:16:35 -03:00
Belen Curcio 4727c52de4 camelCase lodash 2017-11-10 10:48:12 -03:00
Belen Curcio f461d27810 Merge branch 'master' of github.com:coralproject/talk into user-status 2017-11-10 10:46:53 -03:00
Belen Curcio fbc5a9d74b moderate 2017-11-10 10:45:26 -03:00
Belen Curcio 6ff4994d0d lodash.camelcase 2017-11-10 10:33:55 -03:00
Belen Curcio c793a840ea Merge branch 'user-status' of github.com:coralproject/talk into user-status 2017-11-10 10:27:28 -03:00
Belen Curcio ebda51dc0f Adding suspension test 2017-11-10 10:26:54 -03:00
Belen Curcio b0573749ee Adding classes and missingproptypes 2017-11-10 10:26:34 -03:00
Chi Vinh Le 933903de2f Remove IE 64bit from ci until they fix the keyboard input issues 2017-11-10 14:05:43 +01:00
Kim Gardner 4373c382e1 Merge pull request #1151 from coralproject/feature/fix-email-config-docs
Fix incorrect email configuration in docs
2017-11-10 12:02:22 +00:00
Kim Gardner af6787bc0f Merge branch 'master' into feature/fix-email-config-docs 2017-11-10 11:37:15 +00:00
Chi Vinh Le dd1fbf30ca Add navigateAndLogin 2017-11-10 10:51:30 +01:00
Chi Vinh Le a5aa59e255 Merge branch 'master' into user-status 2017-11-10 10:09:29 +01:00
Kiwi 19b8d0a6ea Merge pull request #1150 from coralproject/e2e-v2
e2e v2
2017-11-10 10:08:27 +01:00
Chi Vinh Le bcf8370359 Temporarily enable bs testing on this branch 2017-11-10 10:04:22 +01:00
Wyatt Johnson 0da0b1a37e Merge branch 'master' into feature/fix-email-config-docs 2017-11-09 16:23:24 -07:00
Chi Vinh Le 31ee8139b6 Use visible were suitable and wait for drawer to dissappear 2017-11-09 23:32:03 +01:00
Wyatt Johnson 51977dcf54 Merge branch 'master' into bump-version-3-8-0 2017-11-09 15:10:24 -07:00
Chi Vinh Le 0b18bb06e7 More tests 2017-11-09 22:31:53 +01:00
Chi Vinh Le 9abb3d30da e2e fixes 2017-11-09 22:04:32 +01:00
Chi Vinh Le f0839c57df Change to allowed name 2017-11-09 21:52:08 +01:00
Chi Vinh Le b2929e9a83 Fail fast 2017-11-09 21:51:41 +01:00
Chi Vinh Le ca7b790962 Return this 2017-11-09 21:51:31 +01:00
Chi Vinh Le 42a477ae41 Fix firefox popup bug e2e 2017-11-09 21:51:03 +01:00
Chi Vinh Le 53461df287 Increase wait for iframe time 2017-11-09 19:30:22 +01:00
Kiwi 9b18a78ce0 Merge branch 'master' into user-status 2017-11-09 19:20:06 +01:00
Chi Vinh Le da35f39baf Set resolution 2017-11-09 19:19:47 +01:00
Chi Vinh Le e885178dfc Refactor 2 2017-11-09 19:03:01 +01:00
Kim Gardner 24e8aab0fe Merge branch 'master' into e2e-v2 2017-11-09 17:09:56 +00:00
Kim Gardner 44d6ad340d Merge pull request #1156 from coralproject/queue-fix
Queue fix for custom assets
2017-11-09 17:09:25 +00:00
Chi Vinh Le 63b6315435 Refactor #1 2017-11-09 17:50:47 +01:00
Kiwi 95ca43ff8c Merge branch 'master' into e2e-v2 2017-11-09 16:23:20 +01:00
Chi Vinh Le 55e4aed511 Merge branch 'master' into user-status 2017-11-09 15:53:42 +01:00
Kim Gardner 6d5adc8f54 Bump version number to 3.8.0 2017-11-09 13:19:05 +00:00
Kim Gardner 247e05383e One more fix 2017-11-09 11:23:48 +00:00
Kim Gardner 3988fb9e93 Merge branch 'feature/fix-email-config-docs' of git+ssh://github.com/coralproject/talk into feature/fix-email-config-docs 2017-11-09 11:20:07 +00:00
Kim Gardner 7d524efaad Merge branch 'master' into feature/fix-email-config-docs 2017-11-09 11:19:57 +00:00
Kim Gardner d7a88cdc2a Fix config key 2017-11-09 11:18:58 +00:00
Wyatt Johnson 18f5b0cc5e queue fix for sometimes null settings 2017-11-08 14:24:48 -07:00
Kiwi 2df18f07cb Merge pull request #1140 from coralproject/user-pending
Users with PENDING status should not be allowed to post & E2E for User Status
2017-11-08 19:27:41 +01:00
Chi Vinh Le 786e85686d Fix invalid regexp for ie 2017-11-08 18:57:06 +01:00
Belen Curcio 9a651880a2 Updated login 2017-11-08 14:00:01 -03:00
Belen Curcio 82e01eac00 Merge branch 'master' of github.com:coralproject/talk into user-status 2017-11-08 13:56:48 -03:00
Belen Curcio 5efdc9865f refacotr 2017-11-08 12:59:08 -03:00
Belen Curcio 62d01cf2a0 perform login 2017-11-08 12:31:33 -03:00
Belen Curcio 81bf63836a tests 2017-11-08 12:23:40 -03:00
Belen Curcio 54ac2985ba Adding classes 2017-11-08 12:23:29 -03:00
Belen Curcio e9bd5703f1 refactor 2017-11-08 11:49:32 -03:00
Jeff Nelson a394ba29b3 Update 02-02-advanced-configuration.md
Change TALK_SMTP_EMAIL to TALK_SMTP_FROM_EMAIL
2017-11-08 07:11:03 -05:00
Belen Curcio a2470fa01b More 2017-11-07 16:43:58 -03:00
Belen Curcio 9b98e3cac9 missing classes 2017-11-07 15:20:14 -03:00
Chi Vinh Le 4f89c1e1c3 Add some comments and description 2017-11-07 17:50:56 +01:00
Chi Vinh Le c3e551deb5 Disable bs for branches 2017-11-07 17:48:12 +01:00
Chi Vinh Le 0678ea0344 Small tweak 2017-11-07 17:07:50 +01:00
Chi Vinh Le b09b56bd24 Temporarily enable browserstack 2017-11-07 16:52:27 +01:00
Chi Vinh Le 2a0f70f084 Clean DB before serve 2017-11-07 16:42:37 +01:00
Kiwi bf2e11ef9a Merge branch 'master' into e2e-v2 2017-11-07 16:29:51 +01:00
Chi Vinh Le 0fc810a7b1 e2e v2 2017-11-07 16:21:09 +01:00
Belen Curcio 918ad915b8 Tests 2017-11-06 23:53:33 -03:00
Belen Curcio d51b6b31f6 Adding missing classes and proptypes 2017-11-06 23:53:19 -03:00
Belén Curcio e88445beac Merge branch 'master' into user-pending 2017-11-06 13:48:26 -03:00
Belen Curcio 33be7531a0 Merge branch 'master' of github.com:coralproject/talk 2017-11-06 12:58:15 -03:00
Devon Jue 2131638f48 undo reject comment on stream 2017-11-04 17:17:30 -07:00
Kim Gardner 39a980b22f Merge pull request #1146 from coralproject/isStaff
Use isStaff instead
2017-11-01 17:38:37 +00:00
Chi Vinh Le ce0b63fe75 Use isStaff instead 2017-11-01 13:29:04 +01:00
Kim Gardner c0cb641df3 Merge branch 'master' into user-pending 2017-11-01 11:27:04 +00:00
Kiwi cee68ae471 Merge pull request #1142 from PepeFranco/reportedComments
Reported comments
2017-11-01 12:17:25 +01:00
Kiwi 33cd783b37 Merge branch 'master' into reportedComments 2017-11-01 12:10:39 +01:00
Kiwi 82ef16a4ee Merge pull request #1144 from blackcathacker/queue_shortcuts
add numbered shortcuts for mod queue
2017-11-01 12:10:17 +01:00
Kiwi 736ab6066f Merge branch 'master' into reportedComments 2017-11-01 12:06:34 +01:00
Kiwi 3327604a58 Merge branch 'master' into queue_shortcuts 2017-11-01 12:04:03 +01:00
Kiwi 0c3cfd21a9 Merge pull request #1143 from blackcathacker/staff_badge
show staff instead of user badge when reported by staff
2017-11-01 11:59:56 +01:00
blackcathacker 903e70d641 updates per review 2017-10-31 17:10:25 -07:00
blackcathacker 21f6caa942 cleaner method chain per review 2017-10-31 16:50:35 -07:00
PepeFranco f70ccf641a Fixed lint issue 2017-10-31 13:29:44 -06:00
PepeFranco dd4e3d7ec8 Merge branch 'reportedComments' of https://github.com/PepeFranco/talk into reportedComments 2017-10-31 13:00:05 -06:00
PepeFranco 886a468993 Reverting comment changes 2017-10-31 12:56:32 -06:00
PepeFranco de0dad6243 Merge branch 'master' of https://github.com/coralproject/talk into reportedComments
Updating
2017-10-31 12:49:44 -06:00
Kiwi 7226c475e3 Merge branch 'master' into user-pending 2017-10-31 19:25:51 +01:00
Kim Gardner 173c9262d1 Merge branch 'master' into queue_shortcuts 2017-10-31 17:18:59 +00:00
Kim Gardner 5e7ee344ec Merge branch 'master' into staff_badge 2017-10-31 17:18:54 +00:00
Kim Gardner b2d0c58a72 Merge branch 'master' into reportedComments 2017-10-31 17:12:02 +00:00
Kiwi b34b033a18 Merge pull request #1139 from coralproject/bs-stabilize
Stabilize browserstack tests
2017-10-31 17:47:50 +01:00
Chi Vinh Le 4195d8b419 Merge branch 'bs-stabilize' of ssh://github.com/coralproject/talk into bs-stabilize 2017-10-31 17:34:59 +01:00
Chi Vinh Le bfb2da519f Exit code can be negative 2017-10-31 17:34:28 +01:00
Chi Vinh Le 8821c0aa5c Typos 2017-10-31 17:33:44 +01:00
Kiwi a56031ac47 Merge branch 'master' into bs-stabilize 2017-10-31 17:13:25 +01:00
Wyatt Johnson a3eeec34df Merge pull request #1137 from blackcathacker/premod_setting
Send user an email when they are banned #1050
2017-10-31 10:06:00 -06:00
Kim Gardner c4ad667788 Merge branch 'master' into user-pending 2017-10-31 16:03:07 +00:00
Wyatt Johnson d20666adb9 Update users.js
Prefix added by mailer service
2017-10-31 10:00:47 -06:00
Chi Vinh Le 96b44b2b99 Prefix env variables 2017-10-31 16:39:41 +01:00
Wyatt Johnson b92b4027b0 Merge branch 'master' into premod_setting 2017-10-31 09:32:43 -06:00
Jose Franco f1423abdea Merge branch 'master' into reportedComments 2017-10-31 09:31:32 -06:00
Chi Vinh Le c31e6347d0 Sleep between retries 2017-10-31 16:29:32 +01:00
Chi Vinh Le 48179b413c Merge branch 'bs-stabilize' of ssh://github.com/coralproject/talk into bs-stabilize 2017-10-31 16:25:24 +01:00
Kiwi c955fcd29e Merge branch 'master' into bs-stabilize 2017-10-31 16:25:18 +01:00
Kim Gardner 457322e3d0 Merge pull request #1145 from coralproject/readme-updates
Readme updates
2017-10-31 14:40:53 +00:00
Kim Gardner 9ba8cbcbe1 Fix logo link 2017-10-31 14:30:32 +00:00
Kim Gardner 93363a0650 Remove indenting 2017-10-31 14:27:38 +00:00
Chi Vinh Le 7f442ce91e Let MAX_RETRIES be customizable 2017-10-31 14:58:40 +01:00
Chi Vinh Le 42f0d76d98 Add retry number to console outpu 2017-10-31 14:58:25 +01:00
Chi Vinh Le 1578316363 Retry browsers when failed, print summary 2017-10-31 14:25:58 +01:00
Kim Gardner 5c033dbfdf Tweaks 2017-10-31 11:40:33 +00:00
Kim Gardner 3cdf2a3118 Formatting 2017-10-31 11:31:13 +00:00
Kim Gardner 4e5e989d22 Update one-click directions 2017-10-31 11:25:22 +00:00
Kim Gardner 62660ed144 Fix header 2017-10-31 11:18:24 +00:00
Kim Gardner 2d44f7abf4 Spruce up README 2017-10-31 11:12:14 +00:00
Kim Gardner ea98f0275d Merge branch 'master' into premod_setting 2017-10-31 10:35:44 +00:00
blackcathacker 07e6c15c3a add numbered shortcuts for mod queue 2017-10-30 23:08:33 -07:00
blackcathacker 05e91129f4 show staff instead of user badge when reported by staff 2017-10-30 22:16:59 -07:00
PepeFranco 350e679abf Added check for user & comments 2017-10-30 16:58:51 -06:00
PepeFranco ae07dfb2cd Added check for user & comments 2017-10-30 16:56:52 -06:00
Chi Vinh Le 88d012755c Always use windows 10 2017-10-30 23:16:23 +01:00
Chi Vinh Le 0cdefa0066 Fix wrong typed characters 2017-10-30 21:52:36 +01:00
Chi Vinh Le e4688f9876 Pause to let iframe load 2017-10-30 19:08:42 +01:00
Chi Vinh Le e091fa552a Upgrade selenium standalone 2017-10-30 17:02:20 +01:00
Chi Vinh Le 53b1104cf8 Add little pause waiting for popup to open 2017-10-30 16:09:16 +01:00
Chi Vinh Le 975fe550e0 Harmonize local chrome headless window-size 2017-10-30 16:08:03 +01:00
Chi Vinh Le 8adff14263 Reactivate e2e on ci 2017-10-30 15:53:52 +01:00
Kiwi 5f07d0256c Merge branch 'master' into bs-stabilize 2017-10-30 15:40:17 +01:00
Kim Gardner f573504a3c Merge pull request #1141 from coralproject/update-version
Update version number to be correct
2017-10-30 14:34:26 +00:00
Kim Gardner 8c0118106b Update version number to be correct 2017-10-30 14:24:15 +00:00
Kiwi 4ce720e510 Merge branch 'master' into bs-stabilize 2017-10-30 14:46:32 +01:00
Kim Gardner 81c669d63f Merge branch 'master' into user-pending 2017-10-30 13:34:09 +00:00
Chi Vinh Le 97a8f95b93 Use win10 for ie browsers 2017-10-30 14:25:18 +01:00
Kim Gardner 7bf114c01a Merge branch 'master' into premod_setting 2017-10-30 13:25:14 +00:00
Kim Gardner 6460cbd536 Merge pull request #1138 from blackcathacker/disable_report
disable report button when comment already reported
2017-10-30 13:24:10 +00:00
Chi Vinh Le 05b326b91c Add comments 2017-10-30 14:14:49 +01:00
Chi Vinh Le 9af206da1a Correct echo output 2017-10-30 14:08:13 +01:00
Chi Vinh Le 0c3a9db443 Add more console logs 2017-10-30 13:52:11 +01:00
Chi Vinh Le 511f4f0975 Refactor 2017-10-30 13:26:50 +01:00
Chi Vinh Le 95545a0e7d Merge branch 'bs-stabilize' of ssh://github.com/coralproject/talk into bs-stabilize 2017-10-30 13:13:23 +01:00
Chi Vinh Le 12a71fa117 Wait before click 2017-10-30 13:11:41 +01:00
Belen Curcio 302d30cde1 Adding missing classnames and proptypes, plus e2e testing covering userstatus 2017-10-30 08:54:32 -03:00
Kiwi 3da4a70442 Merge branch 'master' into bs-stabilize 2017-10-30 12:50:15 +01:00
Belen Curcio 6d7afc7bbc Adding missing classes and props 2017-10-30 08:07:03 -03:00
Belen Curcio b6786f614e Adding missing props 2017-10-30 07:56:51 -03:00
Belen Curcio a6a903c459 e2e tests 2017-10-30 07:52:08 -03:00
Belen Curcio 34b58779e8 User PENDING status should not be able to post 2017-10-30 07:04:39 -03:00
blackcathacker 82a332c787 disable report button when comment already reported 2017-10-29 19:48:42 -07:00
blackcathacker bfe82218ed Send user an email when they are banned #1050 2017-10-29 18:48:15 -07:00
Chi Vinh Le 5f5c64870a Stabilize e2e 2017-10-27 19:00:48 +02:00
Chi Vinh Le ad4a5595f9 Enable screenshot on failure for bs 2017-10-27 13:19:00 +02:00
Wyatt Johnson b7137fbcd4 Merge pull request #1132 from coralproject/docker-regression
Docker Regression Fix
2017-10-26 09:43:15 -06:00
Chi Vinh Le b84daf0422 Set resolution to 1600x1200 bs 2017-10-26 17:30:55 +02:00
Wyatt Johnson 4089d09681 fix regression missing yarn cache clean 2017-10-26 09:19:29 -06:00
Kiwi a7eb588f7f Merge pull request #1122 from coralproject/onbuild
Docker Updates
2017-10-26 14:40:45 +02:00
Kiwi cd92dbf682 Merge branch 'master' into onbuild 2017-10-26 14:33:02 +02:00
Kiwi 89ab71eb1d Merge pull request #1130 from coralproject/tests-e2e
Missing Classes and testing Admin Drawer
2017-10-26 14:31:58 +02:00
Kiwi c1cfe3b200 Merge branch 'master' into onbuild 2017-10-26 14:28:59 +02:00
Kiwi d7ed679384 Merge branch 'master' into tests-e2e 2017-10-26 14:20:53 +02:00
Belen Curcio 59acdc5c02 new e2e test 2017-10-25 16:53:18 -03:00
Belen Curcio b06ae9001e Adding missing classes 2017-10-25 16:31:46 -03:00
Kim Gardner 1f170c1dca Merge branch 'master' into onbuild 2017-10-24 13:20:24 +01:00
Wyatt Johnson 9217239caf Merge branch 'master' into onbuild 2017-10-23 12:33:56 -06:00
Wyatt Johnson fac028c22b update to docker build process 2017-10-23 10:50:29 -06:00
89 changed files with 1602 additions and 458 deletions
+11 -4
View File
@@ -1,11 +1,18 @@
FROM coralproject/talk:latest
# Setup the build arguments
ONBUILD ARG TALK_THREADING_LEVEL=3
ONBUILD ARG TALK_DEFAULT_STREAM_TAB=all
ONBUILD ARG TALK_DEFAULT_LANG=en
ONBUILD ARG TALK_PLUGINS_JSON
# Bundle app source
ONBUILD COPY . /usr/src/app
# At this stage, we need to install the development dependancies again because
# we need to have webpack available. We then build the new dependancies and
# clear out the development dependancies again. After this we of course need to
# At this stage, we need to install the development dependencies again because
# we need to have webpack available. We then build the new dependencies and
# clear out the development dependencies again. After this we of course need to
# clear out the yarn cache, this saves quite a lot of size.
ONBUILD RUN cli plugins reconcile && \
yarn build
yarn build && \
yarn cache clean
+7 -3
View File
@@ -4,8 +4,12 @@ Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
either express or implied.
See the License for the specific language governing permissions and limitations under the License.
See the License for the specific language governing permissions
and limitations under the License.
+22 -31
View File
@@ -1,46 +1,37 @@
# Talk
# Talk · [![CircleCI](https://circleci.com/gh/coralproject/talk.svg?style=svg)](https://circleci.com/gh/coralproject/talk) · [![NSP Status](https://nodesecurity.io/orgs/coralproject/projects/07ce2e4c-99fb-48f8-b50b-69d2d2c081b8/badge)](https://nodesecurity.io/orgs/coralproject/projects/07ce2e4c-99fb-48f8-b50b-69d2d2c081b8) · [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md#pull-requests)
[![CircleCI](https://circleci.com/gh/coralproject/talk.svg?style=svg)](https://circleci.com/gh/coralproject/talk)
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://dashboard.heroku.com/new?template=https%3A%2F%2Fgithub.com%2Fcoralproject%2Ftalk&env[TALK_FACEBOOK_APP_ID]=ignore&env[TALK_FACEBOOK_APP_SECRET]=ignore)
[![NSP Status](https://nodesecurity.io/orgs/coralproject/projects/07ce2e4c-99fb-48f8-b50b-69d2d2c081b8/badge)](https://nodesecurity.io/orgs/coralproject/projects/07ce2e4c-99fb-48f8-b50b-69d2d2c081b8)
Online comments are broken. Our open-source Talk tool rethinks how moderation, comment display, and conversation function, creating the opportunity for safer, smarter discussions around your work. [Read more about Talk here](https://coralproject.net/products/talk.html).
Online comments are broken. Our open-source commenting platform, Talk, rethinks how moderation, comment display, and conversation function, creating the opportunity for safer, smarter discussions around your work. [Read more about Talk here](https://coralproject.net/products/talk.html).
Built with <3 by The Coral Project & Mozilla.
## Getting Started
## Try Talk!
Check out our Docs: https://coralproject.github.io/talk/
You're just one click away from trying Talk - all you need is a Heroku account and a few minutes of your time.
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://dashboard.heroku.com/new?template=https%3A%2F%2Fgithub.com%2Fcoralproject%2Ftalk&env[TALK_FACEBOOK_APP_ID]=ignore&env[TALK_FACEBOOK_APP_SECRET]=ignore)
## Technical Documentation
From getting up and running, to advanced configuration, to how to scale Talk, our [Talk Technical Docs](https://coralproject.github.io/talk/) have everything you need to know.
## Product Guide
Learn more about Talk, including a deep dive into features for commenters and moderators, and FAQs in our [Talk Product Guide](https://coralproject.github.io/talk/how-talk-works).
## Relevant Links
- Blog: https://blog.coralproject.net/
- Community Forums: https://community.coralproject.net/
- Community Guides for Journalism: https://guides.coralproject.net/
- Project: https://coralproject.net/
- Roadmap: https://www.pivotaltracker.com/n/projects/1863625
- [Our Blog](https://blog.coralproject.net/)
- [Community Forums](https://community.coralproject.net/)
- [Community Guides for Journalism](https://guides.coralproject.net/)
- [More About Us](https://coralproject.net/)
- [Talk Roadmap](https://www.pivotaltracker.com/n/projects/1863625)
## End-to-End Testing
Talk uses [Nightwatch](http://nightwatchjs.org/) to write e2e tests. The testing infrastructure that allows us to run our tests in real browsers is provided with love by our friends at Browserstack.
Talk uses [Nightwatch](https://nightwatchjs.org/) as our e2e testing framework. The testing infrastructure that allows us to run our tests in real browsers is provided with love by our friends at [Browserstack](https://browserstack.com).
![](/public/img/browserstack_logo.png)
[![Browserstack](/public/img/browserstack_logo.png)](https://browserstack.com)
## License
Copyright 2017 Mozilla Foundation
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
either express or implied.
See the License for the specific language governing permissions
and limitations under the License.
Talk is released under the [Apache License, v2.0](/LICENSE).
+1 -1
View File
@@ -50,7 +50,7 @@ test:
- MOCHA_FILE=$CIRCLE_TEST_REPORTS/junit/test-results.xml MOCHA_REPORTER=mocha-junit-reporter yarn test
# Check dependancies using nsp.
- nsp check
# - yarn e2e-ci
- yarn e2e-ci
deployment:
release:
@@ -32,8 +32,9 @@ class ActionsMenu extends React.Component {
};
render() {
const {className = ''} = this.props;
return (
<div className={styles.root} onBlur={this.syncOpenState} >
<div className={cn(styles.root, className)} onBlur={this.syncOpenState} >
<Button
cStyle='actions'
className={cn(styles.button, {[styles.buttonOpen]: this.state.open})}
@@ -59,6 +60,7 @@ class ActionsMenu extends React.Component {
ActionsMenu.propTypes = {
icon: PropTypes.string,
children: PropTypes.node,
className: PropTypes.string,
};
export default ActionsMenu;
@@ -3,12 +3,14 @@ import cn from 'classnames';
import {MenuItem} from 'react-mdl';
import PropTypes from 'prop-types';
import styles from './ActionsMenu.css';
import camelCase from 'lodash/camelCase';
const ActionsMenuItem = (props) =>
<MenuItem className={cn(styles.menuItem, props.className)} {...props} />;
<MenuItem className={cn(styles.menuItem, props.className, 'action-menu-item')} {...props} id={camelCase(props.children)}/>;
ActionsMenuItem.propTypes = {
className: PropTypes.string,
children: PropTypes.string,
};
export default ActionsMenuItem;
@@ -7,11 +7,11 @@ import {Icon} from 'coral-ui';
import t from 'coral-framework/services/i18n';
const ApproveButton = ({active, minimal, onClick}) => {
const ApproveButton = ({active, minimal, onClick, className}) => {
const text = active ? t('modqueue.approved') : t('modqueue.approve');
return (
<button
className={cn(styles.root, {[styles.minimal]: minimal, [styles.active]: active})}
className={cn(styles.root, {[styles.minimal]: minimal, [styles.active]: active}, className)}
onClick={onClick}
>
<Icon name={'done'} className={styles.icon} />
@@ -21,6 +21,7 @@ const ApproveButton = ({active, minimal, onClick}) => {
};
ApproveButton.propTypes = {
className: PropTypes.string,
active: PropTypes.bool,
minimal: PropTypes.bool,
onClick: PropTypes.func,
@@ -1,4 +1,5 @@
import React from 'react';
import cn from 'classnames';
import PropTypes from 'prop-types';
import {Dialog} from 'coral-ui';
import styles from './BanUserDialog.css';
@@ -8,28 +9,34 @@ import t from 'coral-framework/services/i18n';
const BanUserDialog = ({open, onCancel, onPerform, username, info}) => (
<Dialog
className={styles.dialog}
className={cn(styles.dialog, 'talk-ban-user-dialog')}
id="banUserDialog"
open={open}
onCancel={onCancel}
title={t('bandialog.ban_user')}>
<span className={styles.close} onClick={onCancel}>×</span>
<div>
<div className={styles.header}>
<h2>{t('bandialog.ban_user')}</h2>
</div>
<div className={styles.separator}>
<h3>{t('bandialog.are_you_sure', username)}</h3>
<i>{info}</i>
</div>
<div className={styles.buttons}>
<Button cStyle="cancel" className={styles.cancel} onClick={onCancel} raised>
{t('bandialog.cancel')}
</Button>
<Button cStyle="black" className={styles.ban} onClick={onPerform} raised>
{t('bandialog.yes_ban_user')}
</Button>
</div>
<div className={styles.header}>
<h2>{t('bandialog.ban_user')}</h2>
</div>
<div className={styles.separator}>
<h3>{t('bandialog.are_you_sure', username)}</h3>
<i>{info}</i>
</div>
<div className={styles.buttons}>
<Button
className={cn(styles.cancel, 'talk-ban-user-dialog-button-cancel')}
cStyle="cancel"
onClick={onCancel}
raised >
{t('bandialog.cancel')}
</Button>
<Button
className={cn(styles.ban, 'talk-ban-user-dialog-button-confirm')}
cStyle="black"
onClick={onPerform}
raised >
{t('bandialog.yes_ban_user')}
</Button>
</div>
</Dialog>
);
@@ -15,7 +15,15 @@ function generateRegExp(phrases) {
.join('[\\s"?!.]+')
).join('|');
return new RegExp(`(^|[^\\w])(${inner})(?=[^\\w]|$)`, 'iu');
const pattern = `(^|[^\\w])(${inner})(?=[^\\w]|$)`;
try {
return new RegExp(pattern, 'iu');
}
catch (_err) {
// IE does not support unicode support, so we'll create one without.
return new RegExp(pattern, 'i');
}
}
// Generate a regular expression detecting `suspectWords` and `bannedWords` phrases.
@@ -6,10 +6,21 @@ import FlagLabel from 'coral-ui/components/FlagLabel';
import cn from 'classnames';
import styles from './CommentLabels.css';
const staffRoles = ['ADMIN', 'STAFF', 'MODERATOR'];
function isUserFlagged(actions) {
return actions.some((action) => action.__typename === 'FlagAction' && action.user);
}
function getUserFlaggedType(actions) {
return actions
.some((action) =>
action.__typename === 'FlagAction' &&
action.user &&
action.user.roles.some((role) => staffRoles.includes(role))
) ? 'Staff' : 'User';
}
function hasSuspectedWords(actions) {
return actions.some((action) => action.__typename === 'FlagAction' && action.reason === 'Matched suspect word filter');
}
@@ -24,7 +35,7 @@ const CommentLabels = ({comment, comment: {className, status, actions, hasParent
<div className={styles.coreLabels}>
{hasParent && <Label iconName="reply" className={styles.replyLabel}>reply</Label>}
{status === 'PREMOD' && <Label iconName="query_builder" className={styles.premodLabel}>Pre-Mod</Label>}
{isUserFlagged(actions) && <FlagLabel iconName="person">User</FlagLabel>}
{isUserFlagged(actions) && <FlagLabel iconName="person">{getUserFlaggedType(actions)}</FlagLabel>}
{hasSuspectedWords(actions) && <FlagLabel iconName="sms_failed">Suspect</FlagLabel>}
{hasHistoryFlag(actions) && <FlagLabel iconName="sentiment_very_dissatisfied">History</FlagLabel>}
</div>
+9 -6
View File
@@ -5,14 +5,15 @@ import {IndexLink, Link} from 'react-router';
import styles from './Drawer.css';
import t from 'coral-framework/services/i18n';
import {can} from 'coral-framework/services/perms';
import cn from 'classnames';
const CoralDrawer = ({handleLogout, auth = {}}) => (
<Drawer className={styles.drawer}>
<Drawer className={cn('talk-admin-drawer-nav', styles.drawer)}>
{ auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ?
<div>
<Navigation className={styles.nav}>
<IndexLink
className={styles.navLink}
className={cn('talk-admin-nav-dashboard', styles.navLink)}
to="/admin/dashboard"
activeClassName={styles.active}>
{t('configure.dashboard')}
@@ -20,19 +21,21 @@ const CoralDrawer = ({handleLogout, auth = {}}) => (
{
can(auth.user, 'MODERATE_COMMENTS') && (
<Link
className={styles.navLink}
className={cn('talk-admin-nav-moderate', styles.navLink)}
to="/admin/moderate"
activeClassName={styles.active}>
{t('configure.moderate')}
</Link>
)
}
<Link className={styles.navLink}
<Link
className={cn('talk-admin-nav-stories', styles.navLink)}
to="/admin/stories"
activeClassName={styles.active}>
{t('configure.stories')}
</Link>
<Link className={styles.navLink}
<Link
className={cn('talk-admin-nav-community', styles.navLink)}
to="/admin/community"
activeClassName={styles.active}>
{t('configure.community')}
@@ -41,7 +44,7 @@ const CoralDrawer = ({handleLogout, auth = {}}) => (
can(auth.user, 'UPDATE_CONFIG') &&
(
<Link
className={styles.navLink}
className={cn('talk-admin-nav-configure', styles.navLink)}
to="/admin/configure"
activeClassName={styles.active}>
{t('configure.configure')}
@@ -4,34 +4,38 @@ import Modal from 'components/Modal';
import styles from './ModerationKeysModal.css';
import t from 'coral-framework/services/i18n';
const shortcuts = [
{
title: 'modqueue.navigation',
shortcuts: {
'j': 'modqueue.next_comment',
'k': 'modqueue.prev_comment',
'ctrl+f': 'modqueue.toggle_search',
't': 'modqueue.next_queue',
's': 'modqueue.singleview',
'?': 'modqueue.thismenu'
}
},
{
title: 'modqueue.actions',
shortcuts: {
'd': 'modqueue.approve',
'f': 'modqueue.reject'
}
}
];
export default class ModerationKeysModal extends React.Component {
static propTypes = {
open: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
hideShortcutsNote: PropTypes.func.isRequired,
shortcutsNoteVisible: PropTypes.string.isRequired
shortcutsNoteVisible: PropTypes.string.isRequired,
queueCount: PropTypes.number.isRequired
}
buildShortcuts = () => {
return [
{
title: 'modqueue.navigation',
shortcuts: {
'j': 'modqueue.next_comment',
'k': 'modqueue.prev_comment',
'ctrl+f': 'modqueue.toggle_search',
't': 'modqueue.next_queue',
[`1...${this.props.queueCount}`]: 'modqueue.jump_to_queue',
's': 'modqueue.singleview',
'?': 'modqueue.thismenu'
}
},
{
title: 'modqueue.actions',
shortcuts: {
'd': 'modqueue.approve',
'f': 'modqueue.reject'
}
}
];
}
render () {
@@ -51,7 +55,7 @@ export default class ModerationKeysModal extends React.Component {
<Modal open={open} onClose={onClose}>
<h3>{t('modqueue.shortcuts')}</h3>
<div className={styles.container}>
{shortcuts.map((shortcut, i) => (
{this.buildShortcuts().map((shortcut, i) => (
<table className={styles.table} key={i}>
<thead>
<tr>
@@ -7,11 +7,11 @@ import {Icon} from 'coral-ui';
import t from 'coral-framework/services/i18n';
const RejectButton = ({active, minimal, onClick}) => {
const RejectButton = ({active, minimal, onClick, className}) => {
const text = active ? t('modqueue.rejected') : t('modqueue.reject');
return (
<button
className={cn(styles.root, {[styles.minimal]: minimal, [styles.active]: active})}
className={cn(styles.root, {[styles.minimal]: minimal, [styles.active]: active}, className)}
onClick={onClick}
>
<Icon name={'close'} className={styles.icon} />
@@ -21,6 +21,7 @@ const RejectButton = ({active, minimal, onClick}) => {
};
RejectButton.propTypes = {
className: PropTypes.string,
active: PropTypes.bool,
minimal: PropTypes.bool,
onClick: PropTypes.func,
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import {Dialog} from 'coral-ui';
import {RadioGroup, Radio} from 'react-mdl';
import styles from './SuspendUserDialog.css';
import cn from 'classnames';
import Button from 'coral-ui/components/Button';
@@ -87,7 +88,7 @@ class SuspendUserDialog extends React.Component {
<Button cStyle="white" className={styles.cancel} onClick={onCancel} raised>
{t('suspenduser.cancel')}
</Button>
<Button cStyle="black" className={styles.perform} onClick={this.goToStep1} raised>
<Button cStyle="black" className={cn(styles.perform, 'talk-admin-suspend-user-dialog-confirm')} onClick={this.goToStep1} raised>
{t('suspenduser.suspend_user')}
</Button>
</div>
@@ -120,7 +121,7 @@ class SuspendUserDialog extends React.Component {
</Button>
<Button
cStyle="black"
className={styles.perform}
className={cn(styles.perform, 'talk-admin-suspend-user-dialog-send')}
onClick={this.handlePerform}
disabled={this.state.message.length === 0}
raised
@@ -137,7 +138,7 @@ class SuspendUserDialog extends React.Component {
const {step} = this.state;
return (
<Dialog
className={styles.dialog}
className={cn(styles.dialog, 'talk-admin-suspend-user-dialog')}
onCancel={onCancel}
open={open}
>
@@ -76,9 +76,9 @@ const CoralHeader = ({
}
<div className={styles.rightPanel}>
<ul>
<li className={styles.settings}>
<li className={cn(styles.settings, 'talk-admin-header-settings')}>
<div>
<IconButton name="settings" id="menu-settings"/>
<IconButton name="settings" id="menu-settings" className="talk-admin-header-settings-button"/>
<Menu target="menu-settings" align="right">
<MenuItem onClick={() => showShortcuts(true)}>{t('configure.shortcuts')}</MenuItem>
<MenuItem>
@@ -91,7 +91,7 @@ const CoralHeader = ({
Report a bug or give feedback
</a>
</MenuItem>
<MenuItem onClick={handleLogout}>
<MenuItem onClick={handleLogout} className="talk-admin-header-sign-out">
{t('configure.sign_out')}
</MenuItem>
</Menu>
@@ -25,6 +25,7 @@ export default withFragments({
}
user {
id
roles
}
}
${getSlotFragmentSpreads(slots, 'comment')}
@@ -1,4 +1,5 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import SuspendUserDialog from '../components/SuspendUserDialog';
@@ -44,6 +45,12 @@ class SuspendUserDialogContainer extends Component {
}
}
SuspendUserDialogContainer.propTypes = {
open: PropTypes.bool,
hideSuspendUserDialog: PropTypes.func,
username: PropTypes.string,
};
const withOrganizationName = withQuery(gql`
query CoralAdmin_SuspendUserDialog {
__typename
@@ -13,11 +13,11 @@ const CommunityMenu = ({flaggedUsernamesCount = 0}) => {
<div className='mdl-tabs'>
<div className={`mdl-tabs__tab-bar ${styles.tabBar}`}>
<div>
<Link to={flaggedPath} className={`mdl-tabs__tab ${styles.tab}`} activeClassName={styles.active}>
<Link to={flaggedPath} className={`mdl-tabs__tab ${styles.tab} talk-admin-nav-flagged-accounts`} activeClassName={styles.active}>
{t('community.flaggedaccounts')}
<CountBadge count={flaggedUsernamesCount} />
</Link>
<Link to={peoplePath} className={`mdl-tabs__tab ${styles.tab}`} activeClassName={styles.active}>
<Link to={peoplePath} className={`mdl-tabs__tab ${styles.tab} talk-admin-nav-people`} activeClassName={styles.active}>
{t('community.people')}
</Link>
</div>
@@ -1,5 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import t from 'coral-framework/services/i18n';
import EmptyCard from 'coral-admin/src/components/EmptyCard';
import LoadMore from '../../../components/LoadMore';
@@ -23,7 +24,7 @@ class FlaggedAccounts extends React.Component {
const hasResults = users.nodes && !!users.nodes.length;
return (
<div className={styles.container}>
<div className={cn('talk-adnin-community-flagged-accounts', styles.container)}>
<div className={styles.mainFlaggedContent}>
{
hasResults
@@ -70,4 +71,15 @@ class FlaggedAccounts extends React.Component {
}
}
FlaggedAccounts.propTypes = {
users: PropTypes.object,
loadMore: PropTypes.func,
showBanUserDialog: PropTypes.func,
showSuspendUserDialog: PropTypes.func,
showRejectUsernameDialog: PropTypes.func,
approveUser: PropTypes.func,
me: PropTypes.object,
viewUserDetail: PropTypes.func,
};
export default FlaggedAccounts;
@@ -1,18 +1,14 @@
import React from 'react';
import styles from './FlaggedUser.css';
// TODO: Should not rely on plugin.
import PropTypes from 'prop-types';
import cn from 'classnames';
import t from 'coral-framework/services/i18n';
import {username} from 'talk-plugin-flags/helpers/flagReasons';
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
import ApproveButton from 'coral-admin/src/components/ApproveButton';
import RejectButton from 'coral-admin/src/components/RejectButton';
import cn from 'classnames';
import t from 'coral-framework/services/i18n';
// TODO: Should work with custom flags too.
const shortReasons = {
[username.other]: t('community.other'),
[username.spam]: t('community.spam_ads'),
@@ -21,7 +17,6 @@ const shortReasons = {
[username.impersonating]: t('community.impersonating'),
};
// Render a single user for the list
class User extends React.Component {
showSuspenUserDialog = () => this.props.showSuspendUserDialog({
@@ -50,12 +45,10 @@ class User extends React.Component {
} = this.props;
return (
<li
tabIndex={0}
className={cn(className, styles.root, {[styles.rootSelected]: selected})}
>
<div className={styles.container}>
<div className={styles.header}>
<li tabIndex={0}
className={cn(className, styles.root, {[styles.rootSelected]: selected})} >
<div className={cn('talk-admin-community-flagged-user', styles.container)}>
<div className={cn('talk-admin-community-flagged-user-header', styles.header)}>
<div className={styles.author}>
<button
onClick={this.viewAuthorDetail}
@@ -78,8 +71,7 @@ class User extends React.Component {
}
</div>
</div>
<div className={styles.body}>
<div className={cn('talk-admin-community-flagged-user-body', styles.body)}>
<div className={styles.flagged}>
<div className={styles.flaggedByCount}>
<i className={cn('material-icons', styles.flagIcon)}>flag</i>
@@ -127,9 +119,11 @@ class User extends React.Component {
<div className={styles.sideActions}>
<div className={styles.actions}>
<ApproveButton
className="talk-admin-flagged-user-approve-button"
onClick={this.approveUser}
/>
<RejectButton
className="talk-admin-flagged-user-reject-button"
onClick={this.showRejectUsernameDialog}
/>
</div>
@@ -141,4 +135,16 @@ class User extends React.Component {
}
}
User.propTypes = {
showSuspendUserDialog: PropTypes.func,
showBanUserDialog: PropTypes.func,
viewUserDetail: PropTypes.func,
showRejectUsernameDialog: PropTypes.func,
approveUser: PropTypes.func,
user: PropTypes.object,
className: PropTypes.string,
selected: PropTypes.bool,
me: PropTypes.object,
};
export default User;
@@ -1,4 +1,5 @@
import React from 'react';
import cn from 'classnames';
import styles from './styles.css';
import Table from './Table';
import {Icon} from 'coral-ui';
@@ -24,7 +25,7 @@ const People = (props) => {
const hasResults = !!users.length;
return (
<div className={styles.container}>
<div className={cn(styles.container, 'talk-admin-community-people-container')}>
<div className={styles.leftColumn}>
<div className={styles.searchBox}>
<Icon name='search' className={styles.searchIcon}/>
@@ -1,6 +1,6 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import {Dialog, Button} from 'coral-ui';
import styles from './RejectUsernameDialog.css';
@@ -29,12 +29,6 @@ class RejectUsernameDialog extends Component {
state = {email: '', stage: 0}
static propTypes = {
stage: PropTypes.number,
handleClose: PropTypes.func.isRequired,
rejectUsername: PropTypes.func.isRequired
}
componentDidMount() {
this.setState({email: t('reject_username.email_message_reject'), about: t('reject_username.username')});
}
@@ -76,7 +70,7 @@ class RejectUsernameDialog extends Component {
const {stage} = this.state;
return <Dialog
className={styles.suspendDialog}
className={cn(styles.suspendDialog, 'talk-reject-username-dialog')}
id="rejectUsernameDialog"
open={open}
onClose={handleClose}
@@ -96,15 +90,18 @@ class RejectUsernameDialog extends Component {
<div className={styles.emailContainer}>
<textarea
rows={5}
className={styles.emailInput}
className={cn(styles.emailInput, 'talk-reject-username-dialog-suspension-message')}
value={this.state.email}
onChange={this.onEmailChange}/>
</div>
</div>
}
<div className={styles.modalButtons}>
<div className={cn(styles.modalButtons, 'talk-reject-username-dialog-buttons')}>
{Object.keys(stages[stage].options).map((key, i) => (
<Button key={i} onClick={this.onActionClick(stage, i)}>
<Button
key={i}
className={cn('talk-reject-username-dialog-button', `talk-reject-username-dialog-button-${key}`)}
onClick={this.onActionClick(stage, i)} >
{t(stages[stage].options[key], t('reject_username.username'))}
</Button>
))}
@@ -114,4 +111,12 @@ class RejectUsernameDialog extends Component {
}
}
RejectUsernameDialog.propTypes = {
stage: PropTypes.number,
handleClose: PropTypes.func.isRequired,
rejectUsername: PropTypes.func.isRequired,
user: PropTypes.object,
open: PropTypes.bool,
};
export default RejectUsernameDialog;
@@ -42,7 +42,7 @@ const Table = ({users, setRole, onHeaderClickHandler, setCommenterStatus, viewUs
</thead>
<tbody>
{users.map((row, i)=> (
<tr key={i}>
<tr key={i} className="talk-admin-community-people-row">
<td className="mdl-data-table__cell--non-numeric">
<button onClick={() => {viewUserDetail(row.id);}} className={cn(styles.username, styles.button)}>{row.username}</button>
<span className={styles.email}>{row.profiles.map(({id}) => id)}</span>
@@ -52,6 +52,7 @@ const Table = ({users, setRole, onHeaderClickHandler, setCommenterStatus, viewUs
</td>
<td className="mdl-data-table__cell--non-numeric">
<Dropdown
containerClassName="talk-admin-community-people-dd-status"
value={row.status}
placeholder={t('community.status')}
onChange={(status) => setCommenterStatus(row.id, status)}>
@@ -61,6 +62,7 @@ const Table = ({users, setRole, onHeaderClickHandler, setCommenterStatus, viewUs
</td>
<td className="mdl-data-table__cell--non-numeric">
<Dropdown
containerClassName="talk-admin-community-people-dd-role"
value={row.roles[0] || ''}
placeholder={t('community.role')}
onChange={(role) => setRole(row.id, role)}>
@@ -73,7 +73,7 @@ class Comment extends React.Component {
return (
<li
tabIndex={0}
className={cn(className, 'mdl-card', selectionStateCSS, styles.root, {[styles.selected]: selected})}
className={cn(className, 'mdl-card', selectionStateCSS, styles.root, {[styles.selected]: selected}, 'talk-admin-moderate-comment')}
id={`comment_${comment.id}`}
>
<div className={styles.container}>
@@ -95,7 +95,7 @@ class Comment extends React.Component {
: null
}
{currentUserId !== comment.user.id &&
<ActionsMenu icon="not_interested">
<ActionsMenu icon="not_interested" className="talk-admin-moderate-comment-actions-menu">
<ActionsMenuItem
disabled={comment.user.status === 'BANNED'}
onClick={this.showSuspendUserDialog}>
@@ -34,6 +34,8 @@ class Moderation extends Component {
key('k', () => this.select(false));
key('f', () => this.moderate(false));
key('d', () => this.moderate(true));
this.getMenuItems()
.forEach((menuItem, idx) => key(`${idx + 1}`, () => this.selectQueue(menuItem)));
}
onClose = () => {
@@ -41,20 +43,23 @@ class Moderation extends Component {
}
nextQueue = () => {
const queueConfig = this.props.queueConfig;
const activeTab = this.props.activeTab;
const assetId = this.props.data.variables.asset_id;
const menuItems = Object.keys(queueConfig).map((queue) => ({
key: queue
}));
const menuItems = this.getMenuItems();
const activeTabIndex = menuItems.findIndex((item) => item.key === activeTab);
const activeTabIndex = menuItems.findIndex((item) => item === activeTab);
const nextQueueIndex = (activeTabIndex === menuItems.length - 1) ? 0 : activeTabIndex + 1;
this.props.router.push(this.props.getModPath(menuItems[nextQueueIndex].key, assetId));
this.selectQueue(menuItems[nextQueueIndex]);
}
selectQueue = (key) => {
const assetId = this.props.data.variables.asset_id;
this.props.router.push(this.props.getModPath(key, assetId));
}
getMenuItems = () => Object.keys(this.props.queueConfig);
closeSearch = () => {
const {toggleStorySearch} = this.props;
toggleStorySearch(false);
@@ -175,6 +180,8 @@ class Moderation extends Component {
key.unbind('k');
key.unbind('f');
key.unbind('d');
this.getMenuItems()
.forEach((menuItem, idx) => key.unbind(`${idx + 1}`));
}
componentWillReceiveProps(nextProps) {
@@ -276,6 +283,7 @@ class Moderation extends Component {
shortcutsNoteVisible={moderation.shortcutsNoteVisible}
open={moderation.modalOpen}
onClose={this.onClose}
queueCount={this.getMenuItems().length}
/>
</div>
<StorySearch
@@ -47,6 +47,9 @@ export default withFragments({
editing {
edited
}
status_history {
type
}
hasParent
${getSlotFragmentSpreads(slots, 'comment')}
...${getDefinitionName(CommentLabels.fragments.comment)}
@@ -71,10 +71,10 @@ class ModerationContainer extends Component {
const id = getAssetId(this.props);
const tab = getTab(this.props);
// Grab premod from asset or from settings
const premod = !id ? settings.moderation : asset.settings.moderation;
// Grab premod from asset or from settings if it's defined.
const setting = id && asset && asset.settings ? asset.settings.moderation : settings.moderation;
const queue = isPremod(premod) ? 'premod' : 'new';
const queue = isPremod(setting) ? 'premod' : 'new';
const activeTab = tab ? tab : queue;
return activeTab;
@@ -111,6 +111,17 @@ class ModerationContainer extends Component {
return this.handleCommentChange(prev, comment, notifyText);
},
},
{
document: COMMENT_RESET_SUBSCRIPTION,
variables,
updateQuery: (prev, {subscriptionData: {data: {commentReset: comment}}}) => {
const user = comment.status_history[comment.status_history.length - 1].assigned_by;
const notifyText = this.props.auth.user.id === user.id
? ''
: t('modqueue.notify_reset', user.username, prepareNotificationText(comment.body));
return this.handleCommentChange(prev, comment, notifyText);
},
},
{
document: COMMENT_EDITED_SUBSCRIPTION,
variables,
@@ -299,6 +310,23 @@ const COMMENT_REJECTED_SUBSCRIPTION = gql`
${Comment.fragments.comment}
`;
const COMMENT_RESET_SUBSCRIPTION = gql`
subscription CommentReset($asset_id: ID){
commentReset(asset_id: $asset_id){
...${getDefinitionName(Comment.fragments.comment)}
status_history {
type
created_at
assigned_by {
id
username
}
}
}
}
${Comment.fragments.comment}
`;
const LOAD_MORE_QUERY = gql`
query CoralAdmin_Moderation_LoadMore($limit: Int = 10, $cursor: Cursor, $sortOrder: SORT_ORDER, $asset_id: ID, $tags:[String!], $statuses:[COMMENT_STATUS!], $action_type: ACTION_TYPE) {
comments(query: {limit: $limit, cursor: $cursor, asset_id: $asset_id, statuses: $statuses, sortOrder: $sortOrder, action_type: $action_type, tags: $tags}) {
@@ -158,6 +158,7 @@
}
.content {
word-wrap: break-word;
}
.footer {
@@ -182,6 +182,7 @@ export default class Comment extends React.Component {
editableUntil: PropTypes.string,
})
}).isRequired,
setCommentStatus: PropTypes.func.isRequired,
// edit a comment, passed (id, asset_id, { body })
editComment: PropTypes.func,
@@ -343,7 +344,12 @@ export default class Comment extends React.Component {
} = this.props;
if (!highlighted && this.commentIsRejected(comment)) {
return <CommentTombstone action='reject' />;
return <CommentTombstone action='reject' onUndo={() => {
this.props.setCommentStatus({
commentId: comment.id,
status: comment.status_history[comment.status_history.length - 2].type,
});
}}/>;
}
if (this.commentIsIgnored(comment)) {
@@ -3,4 +3,10 @@
text-align: center;
padding: 1em;
color: #3E4F71;
}
.undo {
cursor: pointer;
text-decoration: underline;
margin-left: 5px;
}
@@ -24,6 +24,9 @@ class CommentTombstone extends React.Component {
<hr aria-hidden={true} />
<p className={styles.commentTombstone}>
{this.getCopy()}
{this.props.action === 'reject' &&
<span className={styles.undo} onClick={this.props.onUndo}>{t('comment.undo_reject')}</span>
}
</p>
</div>
);
@@ -32,6 +35,7 @@ class CommentTombstone extends React.Component {
CommentTombstone.propTypes = {
action: PropTypes.string,
onUndo: PropTypes.func,
};
export default CommentTombstone;
@@ -213,12 +213,14 @@ class Stream extends React.Component {
const open = !asset.isClosed;
const banned = user && user.status === 'BANNED';
const pending = user && user.status === 'PENDING';
const temporarilySuspended =
user &&
user.suspension.until &&
new Date(user.suspension.until) > new Date();
const showCommentBox = loggedIn && ((!banned && !temporarilySuspended && !highlightedComment) || keepCommentBox);
const showCommentBox = loggedIn && ((!banned && !pending & !temporarilySuspended && !highlightedComment) || keepCommentBox);
const slotProps = {data};
const slotQueryData = {root, asset};
@@ -1,6 +1,10 @@
.editNameInput {
margin-top: 10px;
margin-bottom: 10px;
padding: 10px;
border-radius: 3px;
border: solid 1px #d8d8d8;
font-size: 0.9em;
}
.alert {
@@ -1,5 +1,6 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import t from 'coral-framework/services/i18n';
import styles from './SuspendAccount.css';
import {Button} from 'coral-ui';
@@ -66,17 +67,16 @@ class SuspendedAccount extends Component {
</label>
<input
type='text'
className={styles.editNameInput}
className={cn(styles.editNameInput, 'talk-suspended-account-username-input')}
value={username}
placeholder={t('framework.edit_name.label')}
id='username'
onChange={(e) => this.setState({username: e.target.value})}
rows={3}/><br/>
<Button
onClick={this.onSubmitClick}>
{
t('framework.edit_name.button')
}
className="talk-suspended-account-submit-button"
onClick={this.onSubmitClick} >
{t('framework.edit_name.button')}
</Button>
</div> : null
}
@@ -3,6 +3,7 @@ import React from 'react';
import Comment from '../components/Comment';
import {withFragments} from 'coral-framework/hocs';
import {getSlotFragmentSpreads} from 'coral-framework/utils';
import {withSetCommentStatus} from 'coral-framework/graphql/mutations';
import {THREADING_LEVEL} from '../constants/stream';
import hoistStatics from 'recompose/hoistStatics';
import {nest} from '../graphql/utils';
@@ -75,6 +76,9 @@ const singleCommentFragment = gql`
id
username
}
status_history {
type
}
action_summaries {
__typename
count
@@ -130,6 +134,7 @@ const withCommentFragments = withFragments({
const enhance = compose(
withAnimateEnter,
withCommentFragments,
withSetCommentStatus,
);
export default enhance(Comment);
@@ -102,6 +102,9 @@ export default {
}
}
}
status_history {
type
}
action_summaries {
count
current_user {
@@ -182,6 +185,7 @@ export default {
editableUntil: new Date().toISOString(),
edited: false,
},
status_history: [],
id: `pending-${uuid()}`,
}
}
@@ -1,16 +1,25 @@
import React from 'react';
import PropTypes from 'prop-types';
import t from 'coral-framework/services/i18n';
import RestrictedMessageBox from './RestrictedMessageBox';
export default ({children, restricted, message = t('framework.content_not_available'), restrictedComp}) => {
const RestrictedContent = ({children, restricted, message = t('framework.content_not_available'), restrictedComp}) => {
if (restricted) {
return restrictedComp ? restrictedComp : <RestrictedMessageBox message={message} />;
} else {
return (
<div>
<div className="talk-restricted-content">
{children}
</div>
);
}
};
RestrictedContent.propTypes = {
children: PropTypes.node,
restricted: PropTypes.bool,
message: PropTypes.string,
restrictedComp: PropTypes.node,
};
export default RestrictedContent;
@@ -1,4 +1,13 @@
import React from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import styles from './RestrictedMessageBox.css';
export default ({children}) => <div className={styles.message}>{children}</div>;
const RestrictedMessageBox = ({children}) =>
<div className={cn(styles.message, 'talk-restricted-message-box')}>{children}</div>;
RestrictedMessageBox.propTypes = {
children: PropTypes.node,
};
export default RestrictedMessageBox;
+14 -2
View File
@@ -3,6 +3,7 @@ import cn from 'classnames';
import styles from './Slot.css';
import {connect} from 'react-redux';
import omit from 'lodash/omit';
import kebabCase from 'lodash/kebabCase';
import PropTypes from 'prop-types';
import isEqual from 'lodash/isEqual';
import {getShallowChanges} from 'coral-framework/utils';
@@ -58,6 +59,7 @@ class Slot extends React.Component {
childFactory,
defaultComponent: DefaultComponent,
queryData,
fill,
} = this.props;
const {plugins} = this.context;
let children = this.getChildren();
@@ -72,7 +74,7 @@ class Slot extends React.Component {
}
return (
<Component className={cn({[styles.inline]: inline, [styles.debug]: pluginConfig.debug}, className)}>
<Component className={cn({[styles.inline]: inline, [styles.debug]: pluginConfig.debug}, className, `talk-slot-${kebabCase(fill)}`)}>
{children}
</Component>
);
@@ -85,12 +87,22 @@ Slot.defaultProps = {
Slot.propTypes = {
fill: PropTypes.string.isRequired,
inline: PropTypes.bool,
className: PropTypes.string,
reduxState: PropTypes.object,
defaultComponent: PropTypes.oneOfType([
PropTypes.func,
PropTypes.string,
]),
/**
* You may specify the component to use as the root wrapper.
* Defaults to 'div'.
*/
component: PropTypes.any,
component: PropTypes.oneOfType([
PropTypes.func,
PropTypes.string,
]),
// props coming from graphql must be passed through this property.
queryData: PropTypes.object,
@@ -138,6 +138,9 @@ export const withSetCommentStatus = withMutation(
const fragment = gql`
fragment Talk_SetCommentStatus on Comment {
status
status_history {
type
}
}`;
const fragmentId = `Comment_${commentId}`;
@@ -145,6 +148,8 @@ export const withSetCommentStatus = withMutation(
const data = proxy.readFragment({fragment, id: fragmentId});
data.status = status;
data.status_history = data.status_history ? data.status_history : [];
data.status_history.push({__typename: 'CommentStatusHistory', type: status});
proxy.writeFragment({fragment, id: fragmentId, data});
}
+2 -2
View File
@@ -10,12 +10,12 @@ export default class Dialog extends Component {
onCancel: PropTypes.func,
onClose: PropTypes.func,
open: PropTypes.bool,
style: PropTypes.object
style: PropTypes.object,
};
static defaultProps = {
onCancel: (e) => e.preventDefault(),
onClose: (e) => e.preventDefault()
onClose: (e) => e.preventDefault(),
};
componentDidMount(){
+2 -2
View File
@@ -131,7 +131,7 @@ class Dropdown extends React.Component {
const {containerClassName, toggleClassName, toggleOpenClassName} = this.props;
return (
<ClickOutside onClickOutside={this.hideMenu}>
<div className={cn(styles.dropdown, containerClassName)}>
<div className={cn(styles.dropdown, containerClassName, 'dd dd-container')}>
<div
className={cn(styles.toggle, toggleClassName, {[cn(this.state.isOpen, toggleOpenClassName)]: this.state.isOpen})}
onClick={this.handleClick}
@@ -149,7 +149,7 @@ class Dropdown extends React.Component {
{this.state.isOpen &&
<div>
<div tabIndex="0" onFocus={this.trapFocusBegin} />
<ul className={cn(styles.list, {[styles.listActive] : this.state.isOpen})}>
<ul className={cn(styles.list, {[styles.listActive] : this.state.isOpen}, 'dd-list-active')}>
{React.Children.toArray(this.props.children)
.map((child, i) =>
React.cloneElement(child, {
+3 -1
View File
@@ -16,8 +16,9 @@ class Option extends React.Component {
render() {
const {className, label = '', onClick, onKeyDown} = this.props;
const id = this.props.id ? this.props.id : this.props.value;
return (
<li className={cn(styles.option, className)} onClick={onClick} onKeyDown={onKeyDown} role="option" tabIndex="0" ref={this.handleRef}>
<li className={cn(styles.option, className, 'dd-option')} onClick={onClick} onKeyDown={onKeyDown} role="option" tabIndex="0" ref={this.handleRef} id={id}>
{label}
</li>
);
@@ -26,6 +27,7 @@ class Option extends React.Component {
Option.propTypes = {
className: PropTypes.string,
id: PropTypes.string,
label: PropTypes.string,
onClick: PropTypes.func,
onKeyDown: PropTypes.func,
@@ -158,9 +158,16 @@ export default class FlagButton extends Component {
<ClickOutside onClickOutside={this.handleClickOutside}>
<div className={`${name}-container`}>
<button
disabled={flagged}
ref={(ref) => this.flagButton = ref}
onClick={!this.props.banned && !flaggedByCurrentUser && !localPost ? this.onReportClick : null}
className={cn(`${name}-button`, {[`${name}-button-flagged`]: flagged}, styles.button)}>
className={
cn(`${name}-button`, {
[`${name}-button-flagged`]: flagged,
[styles.flaggedButton]: flagged
},
styles.button)}
>
{
flagged
? <span className={`${name}-button-text`}>{t('reported')}</span>
@@ -3,6 +3,10 @@
margin: 5px 0px 5px 10px;
}
button[disabled] {
cursor: default;
}
.flaggedIcon {
color: #f00
}
+14 -1
View File
@@ -13,6 +13,18 @@
border-bottom: solid 1px #EBEBEB;
}
.main {
min-width: 70%;
}
.sidebar {
min-width: 30%;
}
.commentBody {
word-wrap: break-word;
}
.assetURL {
text-decoration: none;
font-weight: bold;
@@ -44,7 +56,8 @@
margin-top: 0;
margin-bottom: 0;
list-style-type: none;
min-width: 136px;
min-width: 140px;
padding: 0px 10px;
}
li {
+1 -1
View File
@@ -19,7 +19,7 @@ class Comment extends React.Component {
return (
<div className={styles.myComment}>
<div>
<div className={styles.main}>
<Slot
fill="commentContent"
defaultComponent={CommentContent}
+7 -2
View File
@@ -95,10 +95,12 @@ FROM coralproject/talk:latest-onbuild
And running the following to build the docker image:
```bash
docker build -t my-awesome-talk-image .
docker build -t my-awesome-talk-image --build-arg TALK_DEFAULT_LANG=es .
```
Don't forget to replace `my-awesome-talk-image` with your own image name.
Don't forget to replace `my-awesome-talk-image` with your own image name, and
specify your build variables with the `--build-arg`. Refer to [Dockerfile.onbuild](https://github.com/coralproject/talk/blob/master/Dockerfile.onbuild){:target="_blank"} for the
available build variables.
{: .code-aside}
This accomplishes a lot:
@@ -108,6 +110,9 @@ This accomplishes a lot:
2. Installs any new dependencies that were required by any new plugins.
3. Builds the new static bundles so that they are ready to serve when the image
is running.
4. Specifies a build time variable [TALK_DEFAULT_LANG]({{ "/advanced-configuration/#talk_default_lang" | relative_url }}){:.param}. Refer
to [Dockerfile.onbuild](https://github.com/coralproject/talk/blob/master/Dockerfile.onbuild){:target="_blank"} for the
available build variables.
This means that you can create a repository for your organization that simply
includes the above `Dockerfile`, a directory of `plugins`, and a `plugins.json`
+14 -2
View File
@@ -26,10 +26,18 @@ Configure the duration for which comment counts are cached for, parsed by
## TALK_DEFAULT_LANG
This is a **Build Variable** and must be consumed during build. If using the
[Docker-onbuild]({{ "/installation-from-docker/#onbuild" | relative_url }})
image you can specify it with `--build-arg TALK_DEFAULT_LANG=en`.
Specify the default translation language. (Default `en`)
## TALK_DEFAULT_STREAM_TAB
This is a **Build Variable** and must be consumed during build. If using the
[Docker-onbuild]({{ "/installation-from-docker/#onbuild" | relative_url }})
image you can specify it with `--build-arg TALK_DEFAULT_STREAM_TAB=all`.
Specify the default stream tab in the admin. (Default `all`)
## TALK_DISABLE_AUTOFLAG_SUSPECT_WORDS
@@ -361,12 +369,12 @@ Then all the routes for the API will be expecting to be hit on `/talk/`, such as
can perform the path stripping when serving an upstream proxy, but some CDN's
cannot. You would use this option in the latter situation.
## TALK_SMTP_EMAIL
## TALK_SMTP_FROM_ADDRESS
The email address to send emails from using the SMTP provider in the format:
```plain
TALK_SMTP_EMAIL="The Coral Project" <support@coralproject.net>
TALK_SMTP_FROM_ADDRESS="The Coral Project" <support@coralproject.net>
```
Including the name and email address.
@@ -397,6 +405,10 @@ CDN/Storage url. (Defaults to value of
## TALK_THREADING_LEVEL
This is a **Build Variable** and must be consumed during build. If using the
[Docker-onbuild]({{ "/installation-from-docker/#onbuild" | relative_url }})
image you can specify it with `--build-arg TALK_THREADING_LEVEL=3`.
Specify the maximum depth of the comment thread. (Default `3`)
**Note that a high value for `TALK_THREADING_LEVEL` will result in large
+2 -1
View File
@@ -273,7 +273,8 @@ pre {
p a:not(.plain-link) {
@extend .coral-link;
}
ul:not(.toc__menu) li a {
ul:not(.toc__menu) li a,
ol li a {
@extend .coral-link;
}
}
+5 -2
View File
@@ -33,8 +33,11 @@ const createAction = async ({user = {}, pubsub, loaders: {Comments}}, {item_id,
if (item_type === 'USERS' && action_type === 'FLAG') {
// Set the user as pending if it was a user flag.
await UsersService.setStatus(item_id, 'PENDING');
// Set the user as pending if it was a user flag and user has no Admin, Staff or Moderation roles
let user = await UsersService.findById(item_id);
if(!user.isStaff()){
await UsersService.setStatus(item_id, 'PENDING');
}
}
if (comment) {
+2 -4
View File
@@ -43,13 +43,11 @@ const RootMutation = {
setCommentStatus: async (_, {id, status}, {mutators: {Comment}, pubsub}) => {
const comment = await Comment.setStatus({id, status});
if (status === 'ACCEPTED') {
// Publish the comment status change via the subscription.
pubsub.publish('commentAccepted', comment);
} else if (status === 'REJECTED') {
// Publish the comment status change via the subscription.
pubsub.publish('commentRejected', comment);
} else if (status === 'NONE') {
pubsub.publish('commentReset', comment);
}
},
addTag: async (_, {tag}, {mutators: {Tag}}) => {
+3
View File
@@ -11,6 +11,9 @@ const Subscription = {
commentRejected(comment) {
return comment;
},
commentReset(comment) {
return comment;
},
commentFlagged(comment) {
return comment;
},
+11
View File
@@ -2,6 +2,7 @@ const {
SUBSCRIBE_COMMENT_ACCEPTED,
SUBSCRIBE_COMMENT_REJECTED,
SUBSCRIBE_COMMENT_FLAGGED,
SUBSCRIBE_COMMENT_RESET,
SUBSCRIBE_ALL_COMMENT_EDITED,
SUBSCRIBE_ALL_COMMENT_ADDED,
SUBSCRIBE_ALL_USER_SUSPENDED,
@@ -93,6 +94,16 @@ const setupFunctions = plugins.get('server', 'setupFunctions').reduce((acc, {plu
}
},
}),
commentReset: (options, args) => ({
commentReset: {
filter: (comment, context) => {
if (!context.user || !context.user.can(SUBSCRIBE_COMMENT_RESET)) {
return false;
}
return !args.asset_id || comment.asset_id === args.asset_id;
}
},
}),
userSuspended: (options, args) => ({
userSuspended: {
filter: (user, context) => {
+4
View File
@@ -1376,6 +1376,10 @@ type Subscription {
# Requires the `ADMIN` or `MODERATOR` role.
commentRejected(asset_id: ID): Comment
# Get an update whenever the status of a comment has been reset.
# Requires the `ADMIN` or `MODERATOR` role.
commentReset(asset_id: ID): Comment
# Get an update whenever a user has been suspended.
# `user_id` must match id of current user except for
# users with the `ADMIN` or `MODERATOR` role.
+3
View File
@@ -17,6 +17,7 @@ en:
characters_remaining: "characters remaining"
comment:
anon: "Anonymous"
undo_reject: "Undo"
ban_user: "Ban User"
comment: "Post a comment"
edited: Edited
@@ -263,6 +264,7 @@ en:
notify_accepted: '{0} accepted comment "{1}"'
notify_rejected: '{0} rejected comment "{1}"'
notify_flagged: '{0} flagged comment "{1}"'
notify_reset: '{0} reset status of comment "{1}"'
approve: "Approve"
approved: "Approved"
ban_user: "Ban"
@@ -296,6 +298,7 @@ en:
show_shortcuts: "Show Shortcuts"
singleview: "Toggle single comment edit view"
thismenu: "Open this menu"
jump_to_queue: "Jump to specific queue"
thousand: k
try_these: "Try these"
view_more_shortcuts: "View more shortcuts"
+21 -2
View File
@@ -22,22 +22,34 @@ const nightwatch_config = {
'browserstack.user': process.env.BROWSERSTACK_USER || 'coralproject2',
'browserstack.key': process.env.BROWSERSTACK_KEY,
'browserstack.local': true,
'browserstack.localIdentifier': process.env.BROWSERSTACK_LOCAL_IDENTIFIER ? process.env.BROWSERSTACK_LOCAL_IDENTIFIER : undefined,
'browserstack.debug': true,
// Disable this, as it makes bs slow and brittle.
'browserstack.networkLogs': false,
}
'resolution': '1600x1200',
},
screenshots : {
enabled: true,
on_failure: true,
on_error: true,
path: process.env.REPORTS_FOLDER || './test/e2e/reports',
},
},
chrome: {
desiredCapabilities: {
browser: 'chrome',
browser_version: '62',
os: 'Windows',
os_version: '10',
},
},
firefox: {
desiredCapabilities: {
browser: 'firefox',
browser_version: '56',
os: 'Windows',
os_version: '10',
},
},
safari: {
@@ -52,14 +64,21 @@ const nightwatch_config = {
desiredCapabilities: {
browser: 'internet explorer',
os: 'Windows',
os_version: '8.1',
os_version: '10',
browser_version: '11',
// The x64 bit IEDriver that is used by IE 11 has a known issue with sendKeys where
// it may enter incorrect keys (shift + key).
// This adds a delay for each character as temporary fix as advised from the browserstack support.
'browserstack.customSendKeys': 800,
},
},
edge: {
desiredCapabilities: {
browser: 'edge',
browser_version: '15',
os: 'Windows',
os_version: '10',
},
},
}
+6 -4
View File
@@ -9,13 +9,13 @@ module.exports = {
globals_path: './test/e2e/globals',
selenium: {
start_process: true,
server_path: 'node_modules/selenium-standalone/.selenium/selenium-server/3.5.3-server.jar',
server_path: 'node_modules/selenium-standalone/.selenium/selenium-server/3.6.0-server.jar',
log_path: './test/e2e/',
host: '127.0.0.1',
port: 6666,
cli_args: {
'webdriver.chrome.driver': 'node_modules/selenium-standalone/.selenium/chromedriver/2.32-x64-chromedriver',
'webdriver.gecko.driver': 'node_modules/selenium-standalone/.selenium/geckodriver/0.18.0-x64-geckodriver',
'webdriver.chrome.driver': 'node_modules/selenium-standalone/.selenium/chromedriver/2.33-x64-chromedriver',
'webdriver.gecko.driver': 'node_modules/selenium-standalone/.selenium/geckodriver/0.19.0-x64-geckodriver',
}
},
test_settings: {
@@ -40,10 +40,12 @@ module.exports = {
path: process.env.REPORTS_FOLDER || './test/e2e/reports',
},
},
'chrome': {
},
'chrome-headless': {
desiredCapabilities: {
chromeOptions : {
args: ['--headless', '--disable-gpu', 'window-size=1280,800'],
args: ['--headless', '--disable-gpu', 'window-size=1600,1200'],
},
},
},
+3 -5
View File
@@ -1,6 +1,6 @@
{
"name": "talk",
"version": "3.7.0",
"version": "3.8.0",
"description": "A better commenting experience from Mozilla, The New York Times, and the Washington Post. https://coralproject.net",
"main": "app.js",
"private": true,
@@ -16,9 +16,7 @@
"lint-fix": "yarn lint --fix",
"jest-watch": "TEST_MODE=unit NODE_ENV=test jest --watch",
"e2e-ci": "./scripts/e2e-ci.sh",
"e2e-browserstack": "NODE_ENV=test ./scripts/e2e-browserstack.js --config nightwatch-browserstack.conf.js",
"pree2e": "selenium-standalone install",
"e2e": "NODE_ENV=test nightwatch",
"e2e": "./scripts/e2e.js",
"test": "TEST_MODE=unit NODE_ENV=test jest && TEST_MODE=unit NODE_ENV=test mocha -R ${MOCHA_REPORTER:-spec}",
"test-cover": "TEST_MODE=unit NODE_ENV=test istanbul cover _mocha --report text --check-coverage -- -R spec",
"heroku-postbuild": "./bin/cli plugins reconcile && yarn build",
@@ -172,7 +170,7 @@
"redux": "^3.6.0",
"redux-thunk": "^2.1.0",
"resolve": "^1.4.0",
"selenium-standalone": "6.9.0",
"selenium-standalone": "^6.11.0",
"semver": "^5.4.1",
"simplemde": "^1.11.2",
"smoothscroll-polyfill": "^0.3.5",
+1
View File
@@ -36,6 +36,7 @@ module.exports = {
SUBSCRIBE_COMMENT_ACCEPTED: 'SUBSCRIBE_COMMENT_ACCEPTED',
SUBSCRIBE_COMMENT_REJECTED: 'SUBSCRIBE_COMMENT_REJECTED',
SUBSCRIBE_COMMENT_FLAGGED: 'SUBSCRIBE_COMMENT_FLAGGED',
SUBSCRIBE_COMMENT_RESET: 'SUBSCRIBE_COMMENT_RESET',
SUBSCRIBE_ALL_COMMENT_ADDED: 'SUBSCRIBE_ALL_COMMENT_ADDED',
SUBSCRIBE_ALL_COMMENT_EDITED: 'SUBSCRIBE_ALL_COMMENT_EDITED',
SUBSCRIBE_ALL_USER_SUSPENDED: 'SUBSCRIBE_ALL_USER_SUSPENDED',
+2
View File
@@ -9,6 +9,8 @@ module.exports = (user, perm) => {
return check(user, ['ADMIN', 'MODERATOR']);
case types.SUBSCRIBE_COMMENT_REJECTED:
return check(user, ['ADMIN', 'MODERATOR']);
case types.SUBSCRIBE_COMMENT_RESET:
return check(user, ['ADMIN', 'MODERATOR']);
case types.SUBSCRIBE_ALL_COMMENT_EDITED:
return check(user, ['ADMIN', 'MODERATOR']);
case types.SUBSCRIBE_ALL_COMMENT_ADDED:
@@ -44,6 +44,7 @@
margin: 0;
quotes: '\201c' '\201d';
margin-bottom: 10px;
word-wrap: break-word;
}
.quote:before {
@@ -1,4 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import styles from './styles.css';
import {t} from 'plugin-api/beta/client/services';
import {Icon} from 'plugin-api/beta/client/components/ui';
@@ -6,16 +7,23 @@ import cn from 'classnames';
const isApproved = (status) => (status === 'ACCEPTED');
export default ({approveComment, comment: {status}}) => (
const ApproveCommentAction = ({approveComment, comment: {status}}) => (
isApproved(status) ? (
<span className={styles.approved}>
<Icon name="check_circle" className={styles.icon} />
{t('talk-plugin-moderation-actions.approved_comment')}
</span>
) : (
<button className={cn(styles.button, 'talk-plugin-moderation-actions-reject')} onClick={approveComment}>
<button className={cn(styles.button, 'talk-plugin-moderation-actions-approve')} onClick={approveComment}>
<Icon name="done" className={styles.icon} />
{t('talk-plugin-moderation-actions.approve_comment')}
</button>
)
);
ApproveCommentAction.propTypes = {
approveComment: PropTypes.func,
comment: PropTypes.object,
};
export default ApproveCommentAction;
@@ -1,14 +1,21 @@
import React from 'react';
import PropTypes from 'prop-types';
import styles from './styles.css';
import {t} from 'plugin-api/beta/client/services';
import {Icon} from 'plugin-api/beta/client/components/ui';
import cn from 'classnames';
export default ({onBanUser}) => (
const BanUserAction = ({onBanUser}) => (
<button
className={cn(styles.button, 'talk-plugin-moderation-actions-reject')}
className={cn(styles.button, 'talk-plugin-moderation-actions-ban')}
onClick={onBanUser} >
<Icon name="block" className={styles.icon} />
{t('talk-plugin-moderation-actions.ban_user')}
</button>
);
BanUserAction.propTypes = {
onBanUser: PropTypes.func,
};
export default BanUserAction;
@@ -1,10 +1,12 @@
import React from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import styles from './BanUserDialog.css';
import {t} from 'plugin-api/beta/client/services';
import {Dialog, Button} from 'plugin-api/beta/client/components/ui';
export default ({showBanDialog, closeBanDialog, banUser}) => (
<Dialog open={showBanDialog} className={styles.dialog}>
const BanUserDialog = ({showBanDialog, closeBanDialog, banUser}) => (
<Dialog open={showBanDialog} className={cn(styles.dialog, 'talk-ban-user-dialog')}>
<span className={styles.close} onClick={closeBanDialog}>×</span>
<h2>{t('talk-plugin-moderation-actions.ban_user_dialog_headline')}</h2>
<h3>{t('talk-plugin-moderation-actions.ban_user_dialog_sub')}</h3>
@@ -12,12 +14,28 @@ export default ({showBanDialog, closeBanDialog, banUser}) => (
{t('talk-plugin-moderation-actions.ban_user_dialog_copy')}
</p>
<div className={styles.buttons}>
<Button cStyle="cancel" onClick={closeBanDialog} className={styles.cancel} raised>
<Button
cStyle="cancel"
onClick={closeBanDialog}
className={cn(styles.cancel, 'talk-ban-user-dialog-button-cancel')}
raised >
{t('talk-plugin-moderation-actions.ban_user_dialog_cancel')}
</Button>
<Button cStyle="black" onClick={banUser} className={styles.confirm} raised>
<Button
cStyle="black"
onClick={banUser}
className={cn(styles.confirm, 'talk-ban-user-dialog-button-confirm')}
raised >
{t('talk-plugin-moderation-actions.ban_user_dialog_yes')}
</Button>
</div>
</Dialog>
);
BanUserDialog.propTypes = {
showBanDialog: PropTypes.bool.isRequired,
closeBanDialog: PropTypes.func.isRequired,
banUser: PropTypes.func.isRequired,
};
export default BanUserDialog;
@@ -1,9 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import styles from './Menu.css';
import {t} from 'plugin-api/beta/client/services';
export default ({className = '', children}) => (
const Menu = ({className = '', children}) => (
<div className={cn(styles.menu, className)}>
<h3 className={styles.headline}>
{t('talk-plugin-moderation-actions.moderation_actions')}
@@ -11,3 +12,10 @@ export default ({className = '', children}) => (
{children}
</div>
);
Menu.propTypes = {
className: PropTypes.string,
children: PropTypes.node,
};
export default Menu;
@@ -1,4 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import Menu from './Menu';
import styles from './ModerationActions.css';
@@ -21,7 +22,7 @@ export default class ModerationActions extends React.Component {
<Icon name="keyboard_arrow_down" className={styles.icon} />}
</span>
{menuVisible && (
<Menu>
<Menu className="talk-plugin-modetarion-actions-menu">
<Slot
className="talk-plugin-modetarion-actions-slot"
fill="moderationActions"
@@ -38,3 +39,13 @@ export default class ModerationActions extends React.Component {
);
}
}
ModerationActions.propTypes = {
comment: PropTypes.object,
root: PropTypes.object,
asset: PropTypes.object,
data: PropTypes.object,
menuVisible: PropTypes.bool,
toogleMenu: PropTypes.func,
hideMenu: PropTypes.func,
};
@@ -1,12 +1,19 @@
import React from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import styles from './styles.css';
import {t} from 'plugin-api/beta/client/services';
import {Icon} from 'plugin-api/beta/client/components/ui';
export default ({rejectComment}) => (
const RejectCommentAction = ({rejectComment}) => (
<button className={cn(styles.button, 'talk-plugin-moderation-actions-reject')} onClick={rejectComment}>
<Icon name="clear" className={styles.icon} />
{t('talk-plugin-moderation-actions.reject_comment')}
</button>
);
RejectCommentAction.propTypes = {
rejectComment: PropTypes.func,
};
export default RejectCommentAction;
-51
View File
@@ -1,51 +0,0 @@
#!/usr/bin/env node
const Nightwatch = require('nightwatch');
const browserstack = require('browserstack-local');
const {onshutdown} = require('../bin/util');
async function start() {
try {
const bs_local = new browserstack.Local();
process.mainModule.filename = './node_modules/.bin/nightwatch';
// Code to start browserstack local before start of test
console.log('Connecting local');
Nightwatch.bs_local = bs_local;
bs_local.start({
'key': process.env.BROWSERSTACK_KEY,
'logFile': './test/e2e/bslocal.log'
}, function(error) {
if (error) {
console.error(error);
throw error;
}
console.log('Connected. Now testing...');
Nightwatch.cli(function(argv) {
Nightwatch.CliRunner(argv)
.setup(null, function(){
// Code to stop browserstack local after end of parallel test
bs_local.stop(function(){});
})
.runTests(function(){
// Code to stop browserstack local after end of single test
bs_local.stop(function(){});
});
});
});
onshutdown([
() => bs_local.stop(function(){}),
]);
} catch (ex) {
console.log('There was an error while starting the test runner:\n\n');
process.stderr.write(`${ex.stack}\n`);
process.exit(2);
}
}
start();
+12 -29
View File
@@ -1,37 +1,20 @@
#!/bin/bash
if [[ "${CIRCLE_BRANCH}" == "master" ]]; then
REPORTS_FOLDER=${CIRCLE_TEST_REPORTS:-./test/e2e/reports}
CIRCLE_BRANCH=${CIRCLE_BRANCH:-master}
exitCode=0
# Amount of retries before failure.
E2E_MAX_RETRIES=${E2E_MAX_RETRIES:-1}
browserstack() {
REPORTS_FOLDER="$CIRCLE_TEST_REPORTS/$1" yarn e2e-browserstack -- --env "$1"
# Safari >= 8 has issues connecting to browserstack-local. Safari < 8 is too old.
# IE 64bit has issues with receiving keyboard input. Let's wait for them to fix it.
BROWSERS="chrome,firefox,edge" #ie safari
# Determine exit code.
result=$?
if [ "$result" -gt "0" ]
then
exitCode=$result
fi
# Sleep a bit to let browserstack-local to close properly.
sleep 2
}
# Test using browserstack.
browserstack chrome
browserstack firefox
browserstack ie
browserstack edge
# Safari >= 8 has issues connecting to browserstack-local. Safari < 8 is too old.
# browserstack safari
exit $exitCode
if [[ "${CIRCLE_BRANCH}" == "master" && -n "$BROWSERSTACK_KEY" ]]; then
echo Testing on browserstack
yarn e2e --reports-folder "$REPORTS_FOLDER" --bs-key "$BROWSERSTACK_KEY" --retries "$E2E_MAX_RETRIES" --browsers $BROWSERS
else
# When browserstack is not available test locally using chrome headless.
REPORTS_FOLDER="$CIRCLE_TEST_REPORTS/chrome" yarn e2e -- --env chrome-headless
# Will exit with status of last command.
exit $?
echo Testing locally
yarn e2e --reports-folder "$REPORTS_FOLDER" --retries "$E2E_MAX_RETRIES" --headless
fi
Executable
+207
View File
@@ -0,0 +1,207 @@
#!/usr/bin/env node
process.env['NODE_ENV'] = 'test';
const browserstack = require('browserstack-local');
const {onshutdown, shutdown} = require('../bin/util');
const program = require('commander');
const Table = require('cli-table');
const serve = require('../serve');
const childProcess = require('child_process');
const uuid = require('uuid').v4;
const mongoose = require('../services/mongoose');
// Make things colorful!
require('colors');
function startTunnel(key, localIdentifier) {
const bs_local = new browserstack.Local();
// Code to start browserstack local before start of test
console.log('Connecting local');
return new Promise((resolve, reject) => {
bs_local.start({
key,
logFile: './test/e2e/bslocal.log',
verbose: 'true',
force: 'true',
onlyAutomate: 'true',
localIdentifier,
}, (error) => {
if (error) {
reject(error);
}
resolve();
});
onshutdown([
() => bs_local.stop(function(){}),
]);
});
}
function seleniumInstall() {
return new Promise((resolve, reject) => {
try {
const nw = childProcess.spawn(
'./node_modules/.bin/selenium-standalone',
['install'],
{
stdio: 'inherit',
});
nw.on('close', (code) => {
code === 0 ? resolve() : reject();
});
}
catch (ex) {
reject(ex);
}
});
}
function nightwatch(env, config, reportsFolder, browserstack) {
return new Promise((resolve, reject) => {
try {
const nw = childProcess.spawn(
'./node_modules/.bin/nightwatch',
['--config', config, '--env', env],
{
env: Object.assign({}, process.env, {
'BROWSERSTACK_LOCAL_IDENTIFIER': browserstack.localIdentifier,
'BROWSERSTACK_KEY': browserstack.key,
'BROWSERSTACK_USER': browserstack.user,
'REPORTS_FOLDER': `${reportsFolder}/${env}`,
}),
stdio: 'inherit',
});
nw.on('close', (code) => {
code === 0 ? resolve() : reject();
});
}
catch (ex) {
reject(ex);
}
});
}
function printResults(browsers, succeeded, retries) {
let table = new Table({
head: [
'Browser'.cyan,
'Status'.cyan,
'Retries'.cyan,
]
});
for (let browser of browsers) {
const wasSuccessful = browser in succeeded;
table.push([
browser,
wasSuccessful ? 'success'.green : 'failed'.red,
wasSuccessful ? succeeded[browser] : retries,
]);
}
console.log(table.toString());
}
function printSection(txt) {
console.log('*****************************'.magenta);
console.log(`${'*'.magenta} ${txt.cyan}`);
console.log('*****************************'.magenta);
}
async function runBrowserTests(browsers, config, retries = 1, reportsFolder, browserstack) {
const succeeded = {};
for (let browser of browsers) {
for (let t = 0; t < retries + 1; t++) {
try {
printSection(`e2e test for ${browser} #${t}`);
await nightwatch(browser, config, reportsFolder, browserstack);
succeeded[browser] = t;
console.log(`\n==> Succeeded e2e for ${browser} #${t}\n`.green);
break;
}
catch (ex) {
if (ex) {
console.log('There was an error while starting the test runner:\n\n');
process.stderr.write(`${ex.stack}\n`);
}
console.log(`\n==> Failed e2e for ${browser} #${t}\n`.red);
}
}
}
printResults(browsers, succeeded, retries);
return Object.keys(succeeded).length === browsers.length;
}
async function start(program) {
const localIdentifier = uuid();
let browsers = program.browsers.split(',');
const retries = Number.parseInt(program.retries);
const browserstack = {};
const date = new Date().toISOString()
.replace(/[T.]/g, '-')
.replace(/:/g, '');
const reportsFolder = `${program.reportsFolder}/${date}`;
let exitCode = 0;
let config = 'nightwatch.conf.js';
try {
if (program.tunnel && program.bsKey) {
await startTunnel(program.bsKey, localIdentifier);
}
console.log('Dropping test database');
await mongoose.connection.dropDatabase();
await serve();
if (program.bsKey) {
config = 'nightwatch-browserstack.conf.js';
browserstack.localIdentifier = localIdentifier;
browserstack.key = program.bsKey;
browserstack.user = program.bsUser;
} else {
// Install selenium standalone.
await seleniumInstall();
if (program.headless) {
browsers = browsers.map((b) => `${b}-headless`);
}
}
const succeeded = await runBrowserTests(
browsers,
config,
retries,
reportsFolder,
browserstack,
);
if (!succeeded) {
exitCode = 1;
}
}
catch (ex) {
console.log('There was an error:\n\n');
process.stderr.write(`${ex.stack}\n`);
process.exit(2);
}
finally {
console.log('Shutting down');
shutdown();
}
process.exit(exitCode);
}
program
.version('0.1.0')
.description('Perform e2e testing locally or if browserstack credentials are provided on browserstack.')
.option('-u, --bs-user [user]', 'Browserstack user', 'coralproject2')
.option('-k, --bs-key [key]', 'Browserstack api key')
.option('--no-tunnel', 'Dont start browserstack-local')
.option('-b, --browsers [list of browsers]', 'Browsers to test', 'chrome')
.option('-r, --retries [number]', 'Number of retries before failing', '1')
.option('--headless', 'Start in headless mode for local e2e')
.option('--reports-folder [folder]', 'Reports folder', './test/e2e/reports')
.parse(process.argv);
start(program);
+1
View File
@@ -0,0 +1 @@
<%= body.replace(/\n/g, '<br />') %>
+1
View File
@@ -0,0 +1 @@
<%= body %>
+2 -1
View File
@@ -515,5 +515,6 @@ module.exports = {
HandleAuthPopupCallback,
HandleGenerateCredentials,
HandleLogout,
CheckBlacklisted
CheckBlacklisted,
CheckRecaptcha,
};
+20 -1
View File
@@ -398,7 +398,7 @@ module.exports = class UsersService {
// TODO: current updating status behavior is weird.
// once a user has been `APPROVED` its status cannot be
// changed anymore.
return UserModel.findOneAndUpdate({
const user = await UserModel.findOneAndUpdate({
id,
status: {
$ne: 'APPROVED'
@@ -410,6 +410,25 @@ module.exports = class UsersService {
}, {
new: true,
});
if (status === 'BANNED') {
let localProfile = user.profiles.find((profile) => profile.provider === 'local');
if (localProfile) {
const options =
{
template: 'banned', // needed to know which template to render!
locals: { // specifies the template locals.
body: 'In accordance with The Coral Projects community guidelines, your account has been banned. You are now longer allowed to comment, flag or engage with our community.'
},
subject: 'Your account has been banned',
to: localProfile.id // This only works if the user has registered via e-mail.
// We may want a standard way to access a user's e-mail address in the future
};
await MailerService.sendSimple(options);
}
}
return user;
}
/**
+1 -2
View File
@@ -1,11 +1,10 @@
const serve = require('../../serve');
const mongoose = require('../../services/mongoose');
const {shutdown} = require('../../bin/util');
module.exports = {
before: async (done) => {
console.log('Dropping test database');
await mongoose.connection.dropDatabase();
await serve();
done();
},
after: (done) => {
+159 -11
View File
@@ -7,17 +7,165 @@ module.exports = {
return this
.waitForElementVisible('body');
},
openDrawer() {
this
.waitForElementVisible('@drawerButton')
.click('@drawerButton');
this.expect.section('@drawer').to.be.visible;
return this.section.drawer;
},
goToModerate() {
this
.click('@moderateNav')
.expect.section('@moderate').to.be.visible;
return this.section.moderate;
},
goToStories() {
this
.click('@storiesNav')
.expect.section('@stories').to.be.visible;
return this.section.stories;
},
goToCommunity() {
this
.click('@communityNav')
.expect.section('@community').to.be.visible;
return this.section.community;
},
logout() {
this
.waitForElementVisible('@settingsButton')
.click('@settingsButton')
.waitForElementVisible('@signOutButton')
.click('@signOutButton');
},
navigateAndLogin(user) {
this
.navigate()
.expect.section('@login').to.be.visible;
return this.section.login.login(user);
},
}],
elements: {
'loginLayout': '.talk-admin-login',
'signInForm': '.talk-admin-login-sign-in',
'emailInput': '.talk-admin-login-sign-in #email',
'passwordInput': '.talk-admin-login-sign-in #password',
'signInButton': '.talk-admin-login-sign-in-button',
'storiesNav': '.talk-admin-nav-stories',
'storiesSection': '.talk-admin-stories',
'communityNav': '.talk-admin-nav-community',
'communitySection': '.talk-admin-community',
'moderationContainer': '.talk-admin-moderation-container'
}
drawerButton: '.mdl-layout__drawer-button',
drawerOverlay: 'div.mdl-layout__obfuscator.is-visible',
storiesNav: '.talk-admin-nav-stories',
communityNav: '.talk-admin-nav-community',
moderateNav: '.talk-admin-nav-moderate',
settingsButton: '.talk-admin-header-settings-button',
signOutButton: '.talk-admin-header-sign-out',
suspendUserDialog: '.talk-admin-suspend-user-dialog',
suspendUserConfirmButton: '.talk-admin-suspend-user-dialog-confirm',
supendUserSendButton: '.talk-admin-suspend-user-dialog-send',
toast: '.toastify',
toastClose: '.toastify__close',
},
sections: {
moderate: {
selector: '.talk-admin-moderation-container',
elements: {
comment: '.talk-admin-moderate-comment',
commentActionMenu: '.talk-admin-moderate-comment-actions-menu',
actionItemSuspendUser: '.action-menu-item#suspendUser',
actionMenuButton: '.talk-admin-moderate-comment-actions-menu #actions-dropdown-0'
}
},
stories: {
selector: '.talk-admin-stories',
},
community: {
selector: '.talk-admin-community',
commands: [{
url: function() {
return `${this.api.launchUrl}/admin/community`;
},
ready() {
return this
.waitForElementVisible('body');
},
goToPeople() {
this
.click('@peopleNav')
.expect.section('@people').to.be.visible;
return this.section.people;
},
}],
elements: {
peopleNav: '.talk-admin-nav-people',
flaggedAccountsNav: '.talk-admin-nav-flagged-accounts',
flaggedAccountsContainer: '.talk-adnin-community-flagged-accounts',
flaggedUser:'.talk-admin-community-flagged-user',
flaggedUserApproveButton: '.talk-admin-flagged-user-approve-button',
flaggedUserRejectButton: '.talk-admin-flagged-user-reject-button',
usernameDialog: '.talk-reject-username-dialog',
usernameDialogButtons: '.talk-reject-username-dialog-buttons',
usernameDialogSuspend: '.talk-reject-username-dialog-button-k',
usernameDialogSuspensionMessage: '.talk-reject-username-dialog-suspension-message'
},
sections: {
people: {
selector: '.talk-admin-community-people-container',
elements: {
firstRow: '.talk-admin-community-people-row:first-child',
dropdownStatus: '.talk-admin-community-people-dd-status',
dropdownRole: '.talk-admin-community-people-dd-role',
dropdownStatusActive: '.talk-admin-community-people-dd-status .dd-list-active',
optionActive: '.dd-option#ACTIVE',
optionBanned: '.dd-option#BANNED',
}
}
}
},
drawer: {
selector: '.talk-admin-drawer-nav',
commands: [{
goToStories() {
this
.click('@storiesButton');
this.parent.expect.section('@stories').to.be.visible;
this.close();
return this.parent.section.stories;
},
goToCommunity() {
this
.click('@communityButton');
this.parent.expect.section('@community').to.be.visible;
this.close();
return this.parent.section.stories;
},
close() {
this.parent
.click('@drawerOverlay')
.waitForElementNotPresent('@drawerOverlay');
return this.parent;
},
}],
elements: {
'storiesButton': '.talk-admin-drawer-nav .talk-admin-nav-stories',
'communityButton': '.talk-admin-drawer-nav .talk-admin-nav-community',
},
},
login: {
commands: [{
login(user) {
this
.waitForElementVisible('@signInForm')
.setValue('@emailInput', user.email)
.setValue('@passwordInput', user.password)
.waitForElementVisible('@signInButton')
.click('@signInButton');
const adminPage = this.api.page.admin();
adminPage.expect.section('@moderate').to.be.visible;
return adminPage.section.moderate;
},
}],
selector: '.talk-admin-login',
elements: {
'signInForm': '.talk-admin-login-sign-in',
'emailInput': '.talk-admin-login-sign-in #email',
'passwordInput': '.talk-admin-login-sign-in #password',
'signInButton': '.talk-admin-login-sign-in-button',
}
},
},
};
+109 -27
View File
@@ -1,16 +1,38 @@
const iframeId = 'coralStreamEmbed_iframe';
const SortedWindowHandler = require('../utils/SortedWindowHandler');
module.exports = {
commands: [{
ready: function() {
this.switchToIframe();
this.expect.section('@comments').to.be.visible;
return this.section.comments;
},
goToProfileSection: function() {
this.waitForElementVisible('@profileTabButton');
this.click('@profileTabButton');
this.expect.section('@profile').to.be.visible;
return this.section.profile;
},
goToCommentsSection: function() {
this.waitForElementVisible('@commentsTabButton');
this.click('@commentsTabButton');
this.expect.section('@comments').to.be.visible;
return this.section.comments;
},
navigateToAsset: function(asset) {
this.api.url(`${this.api.launchUrl}/assets/title/${asset}`);
return this;
},
getEmbedSection: function() {
switchToIframe: function() {
this.waitForElementVisible('@iframe');
// Pause a bit to let iframe initialize in the hope that it'll
// fix https://www.browserstack.com/automate/builds/96419cf46e3d6376a36ae6d3f90934112df1ed91/sessions/224f1a1566c1c8c7859e2e76ece51862200f0173#automate_button
this.api.pause(1000);
this.api.frame(iframeId);
this.expect.section('@embed').to.be.present;
return this.section.embed;
return this;
},
}],
url: function() {
@@ -18,47 +40,107 @@ module.exports = {
},
elements: {
iframe: `#${iframeId}`,
commentsTabButton: '.talk-embed-stream-comments-tab > button',
profileTabButton: '.talk-embed-stream-profile-tab > button',
banDialog: '.talk-ban-user-dialog',
banDialogConfirmButton: '.talk-ban-user-dialog-button-confirm',
},
sections: {
embed: {
comments: {
commands: [{
getProfileSection: function() {
this.click('@profileTabButton');
this.expect.section('@profile').to.be.present;
return this.section.profile;
openLoginPopup(callback) {
const windowHandler = new SortedWindowHandler(this.api);
this
.waitForElementVisible('@signInButton')
.click('@signInButton');
// Wait for window to be created
// https://www.browserstack.com/automate/builds/1ceccf4efb4683b7feb890f45a32b5922b40ed3f/sessions/17b1a79682bef2498cb0be86eac317a08c976b0a#automate_button
this.api.pause(200);
// Focusing on the Login PopUp
windowHandler.windowHandles((handles) => {
this.api.switchWindow(handles[1]);
});
const popup = this.api.page.popup().ready();
callback(popup);
// Give a tiny bit of time to let popup close.
this.api.pause(50);
if (this.api.capabilities.browserName === 'MicrosoftEdge') {
// More time for edge.
// https://www.browserstack.com/automate/builds/1ceccf4efb4683b7feb890f45a32b5922b40ed3f/sessions/7393dbfda8387e43b6d5851f359b0c07db414973
this.api.pause(1000);
}
// Focusing on the Embed Window
windowHandler.windowHandles((handles) => {
this.api.switchWindow(handles[0]);
// For some reasons firefox does not automatically load auth after login.
// https://www.browserstack.com/automate/builds/37650cb4e66c6edce0ba0800a1c1b7e7f74bf991/sessions/7a4e9da69b0f9ecdf8b7fa9150639e47b1532cb0#automate_button
if (this.api.capabilities.browserName === 'firefox') {
this.parent.navigate().ready();
} else {
this.parent.switchToIframe();
}
});
return this;
},
getCommentsSection: function() {
this.click('@commentsTabButton');
this.expect.section('@comments').to.be.present;
return this.section.comments;
logout() {
this
.waitForElementVisible('@logoutButton')
.click('@logoutButton');
},
}],
selector: '#talk-embed-stream-container',
selector: '.talk-embed-stream-comments-tab-pane',
elements: {
logoutButton: '.talk-stream-userbox-logout',
commentsTabButton: '.talk-embed-stream-comments-tab > button',
profileTabButton: '.talk-embed-stream-profile-tab > button',
signInButton: '#coralSignInButton',
commentBoxTextarea: '#commentText',
commentBoxPostButton: '.talk-plugin-commentbox-button',
firstComment: '.talk-stream-comment.talk-stream-comment-level-0',
firstCommentContent: '.talk-stream-comment.talk-stream-comment-level-0 .talk-stream-comment-content',
respectButton: '.talk-stream-comment.talk-stream-comment-level-0 .talk-stream-comment-footer .talk-plugin-respect-button'
flagButton: '.talk-stream-comment.talk-stream-comment-level-0 .talk-plugin-flags-button',
respectButton: '.talk-stream-comment.talk-stream-comment-level-0 .talk-stream-comment-footer .talk-plugin-respect-button',
restrictedMessageBox: '.talk-restricted-message-box',
suspendedAccountInput: '.talk-suspended-account-username-input',
suspendedAccountSubmitButton: '.talk-suspended-account-submit-button',
},
sections: {
profile: {
selector: '.talk-embed-stream-profile-tab-pane',
flag: {
selector: '.talk-plugin-flags-popup',
elements: {
notLoggedIn: '.talk-embed-stream-not-logged-in',
myCommentHistory: '.talk-my-profile-comment-history',
myCommentHistoryReactions: '.talk-my-profile-comment-history .comment-summary .comment-summary-reactions',
myCommentHistoryReactionCount: '.talk-my-profile-comment-history .comment-summary .comment-summary-reactions .comment-summary-reaction-count',
myCommentHistoryComment: '.talk-my-profile-comment-history .my-comment-body'
offensiveUsernameRadio: '.talk-plugin-flags-popup-radio#USERNAME_OFFENSIVE',
flagUsernameRadio: '.talk-plugin-flags-popup-radio#USERS',
flagCommentRadio: '.talk-plugin-flags-popup-radio#COMMENTS',
continueButton: '.talk-plugin-flags-popup-button',
popUpText: '.talk-plugin-flags-popup-text',
spamCommentRadio: '.talk-plugin-flags-popup-radio#COMMENT_SPAM',
}
},
mod: {
selector: '.talk-plugin-moderation-actions',
elements: {
arrow: '.talk-plugin-moderation-actions-arrow',
menu: '.talk-plugin-modetarion-actions-menu',
banButton: '.talk-plugin-moderation-actions-ban',
},
},
comments: {
selector: '.talk-embed-stream-comments-tab-pane',
elements: {},
},
},
},
profile: {
selector: '.talk-embed-stream-profile-tab-pane',
elements: {
notLoggedIn: '.talk-embed-stream-not-logged-in',
myCommentHistory: '.talk-my-profile-comment-history',
myCommentHistoryReactions: '.talk-my-profile-comment-history .comment-summary .comment-summary-reactions',
myCommentHistoryReactionCount: '.talk-my-profile-comment-history .comment-summary .comment-summary-reactions .comment-summary-reaction-count',
myCommentHistoryComment: '.talk-my-profile-comment-history .my-comment-body',
},
},
},
-13
View File
@@ -1,13 +0,0 @@
module.exports = {
elements: {
registerButton: '#coralRegister',
signInButton: '#coralSignInButton',
emailInput: '#email',
usernameInput: '#username',
passwordInput: '#password',
confirmPasswordInput: '#confirmPassword',
signUpButton: '#coralSignUpButton',
signIn: '.coral-sign-in',
loginButton: '#coralLogInButton'
},
};
+41
View File
@@ -0,0 +1,41 @@
module.exports = {
commands: [{
ready() {
return this
.waitForElementVisible('body');
},
login(user) {
return this
.setValue('@emailInput', user.email)
.setValue('@passwordInput', user.password)
.waitForElementVisible('@signIn')
.waitForElementVisible('@loginButton')
.click('@loginButton');
},
register(user) {
return this
.waitForElementVisible('@registerButton')
.click('@registerButton')
.setValue('@emailInput', user.email)
.setValue('@usernameInput', user.username)
.setValue('@passwordInput', user.password)
.setValue('@confirmPasswordInput', user.password)
.waitForElementVisible('@signUpButton')
.click('@signUpButton')
.waitForElementVisible('@signIn')
.waitForElementVisible('@loginButton')
.click('@loginButton');
},
}],
elements: {
registerButton: '#coralRegister',
signInButton: '#coralSignInButton',
emailInput: '#email',
usernameInput: '#username',
passwordInput: '#password',
confirmPasswordInput: '#confirmPassword',
signUpButton: '#coralSignUpButton',
signIn: '.coral-sign-in',
loginButton: '#coralLogInButton'
},
};
+20 -8
View File
@@ -1,12 +1,27 @@
module.exports = {
'@tags': ['install'],
before: (client) => {
client.resizeWindow(1600, 1200);
},
afterEach: (client, done) => {
if (client.currentTest.results.failed) {
throw new Error('Test Case failed, skipping all the rest');
}
done();
},
after: (client) => {
client.end();
},
'User goes to install': (client) => {
const install = client.page.install();
install
.navigate()
.expect.section('@step1').to.be.present;
.expect.section('@step1').to.be.visible;
},
'User clicks get started button': (client) => {
@@ -20,7 +35,7 @@ module.exports = {
const install = client.page.install();
install
.expect.section('@step2').to.be.present;
.expect.section('@step2').to.be.visible;
},
'User fills step 2': (client) => {
const step2 = client.page.install().section.step2;
@@ -36,7 +51,7 @@ module.exports = {
const install = client.page.install();
install
.expect.section('@step3').to.be.present;
.expect.section('@step3').to.be.visible;
},
'User fills step 3': (client) => {
const step3 = client.page.install().section.step3;
@@ -54,7 +69,7 @@ module.exports = {
const install = client.page.install();
install
.expect.section('@step4').to.be.present;
.expect.section('@step4').to.be.visible;
},
'User fills step 4': (client) => {
const step4 = client.page.install().section.step4;
@@ -73,9 +88,6 @@ module.exports = {
const install = client.page.install();
install
.expect.section('@step5').to.be.present;
.expect.section('@step5').to.be.visible;
},
after: (client) => {
client.end();
}
};
+19 -27
View File
@@ -1,49 +1,41 @@
module.exports = {
'@tags': ['admin', 'login'],
beforeEach: (client) => {
// Testing Desktop
client.resizeWindow(1280, 800);
before: (client) => {
client.resizeWindow(1024, 800);
},
afterEach: (client, done) => {
if (client.currentTest.results.failed) {
throw new Error('Test Case failed, skipping all the rest');
}
done();
},
after: (client) => {
client.end();
},
'Admin logs in': (client) => {
const adminPage = client.page.admin();
const {testData: {admin}} = client.globals;
adminPage
.navigate()
.waitForElementVisible('@loginLayout')
.waitForElementVisible('@signInForm')
.setValue('@emailInput', admin.email)
.setValue('@passwordInput', admin.password)
.waitForElementVisible('@signInButton')
.click('@signInButton');
client.pause(3000);
adminPage
.waitForElementVisible('@moderationContainer');
adminPage.navigateAndLogin(admin);
},
'Admin goes to Stories': (client) => {
const adminPage = client.page.admin();
adminPage
.navigate()
.waitForElementVisible('@storiesNav')
.click('@storiesNav')
.waitForElementVisible('@storiesSection');
.openDrawer()
.goToStories();
},
'Admin goes to Community': (client) => {
const adminPage = client.page.admin();
adminPage
.navigate()
.waitForElementVisible('@communityNav')
.click('@communityNav')
.waitForElementVisible('@communitySection');
.openDrawer()
.goToCommunity();
},
after: (client) => {
client.end();
}
};
+37 -85
View File
@@ -1,5 +1,21 @@
module.exports = {
'@tags': ['embedStream', 'login'],
before: (client) => {
client.resizeWindow(1600, 1200);
},
afterEach: (client, done) => {
if (client.currentTest.results.failed) {
throw new Error('Test Case failed, skipping all the rest');
}
done();
},
after: (client) => {
client.end();
},
'creates a new asset': (client) => {
const asset = 'newAssetTest';
const embedStream = client.page.embedStream();
@@ -7,80 +23,43 @@ module.exports = {
embedStream
.navigateToAsset(asset)
.assert.title(asset)
.getEmbedSection();
.ready();
},
'creates an user and user logs in': (client) => {
const {testData: {user}} = client.globals;
const embedStream = client.page.embedStream();
const embed = embedStream
// Go back to default asset.
const comments =
embedStream
.navigate()
.getEmbedSection();
.ready();
embed
.waitForElementVisible('@signInButton')
.click('@signInButton');
client.pause(3000);
// Focusing on the Login PopUp
client.windowHandles((result) => {
const handle = result.value[1];
client.switchWindow(handle);
});
const login = client.page.login();
login
.waitForElementVisible('@registerButton')
.click('@registerButton')
.setValue('@emailInput', user.email)
.setValue('@usernameInput', user.username)
.setValue('@passwordInput', user.password)
.setValue('@confirmPasswordInput', user.password)
.waitForElementVisible('@signUpButton')
.click('@signUpButton')
.waitForElementVisible('@signIn')
.waitForElementVisible('@loginButton')
.click('@loginButton');
// Focusing on the Embed Window
client.windowHandles((result) => {
const handle = result.value[0];
client.switchWindow(handle);
});
comments
.openLoginPopup((popup) => {
popup.register(user);
});
},
'user posts a comment': (client) => {
const embedStream = client.page.embedStream();
const comments = client.page.embedStream().section.comments;
const {testData: {comment}} = client.globals;
const embed = embedStream
.navigate()
.getEmbedSection();
embed
comments
.waitForElementVisible('@commentBoxTextarea')
.setValue('@commentBoxTextarea', comment.body)
.waitForElementVisible('@commentBoxPostButton')
.click('@commentBoxPostButton')
.waitForElementVisible('@firstCommentContent')
.getText('@firstCommentContent', (result) => {
embed.assert.equal(result.value, comment.body);
comments.assert.equal(result.value, comment.body);
});
},
'signed in user sees comment history': (client) => {
const embedStream = client.page.embedStream();
const profile = client.page.embedStream().goToProfileSection();
const {testData: {comment}} = client.globals;
const embed = embedStream
.navigate()
.getEmbedSection();
const profile = embed
.getProfileSection();
profile
.waitForElementVisible('@myCommentHistory')
.waitForElementVisible('@myCommentHistoryComment')
@@ -89,14 +68,7 @@ module.exports = {
});
},
'user sees replies and reactions to comments': (client) => {
const embedStream = client.page.embedStream();
const embed = embedStream
.navigate()
.getEmbedSection();
const profile = embed
.getProfileSection();
const profile = client.page.embedStream().section.profile;
profile
.waitForElementVisible('@myCommentHistory')
@@ -108,17 +80,12 @@ module.exports = {
},
'user goes to the stream and replies and reacts to comment': (client) => {
const embedStream = client.page.embedStream();
const embed = embedStream
.navigate()
.getEmbedSection();
embed
const comments = embedStream.goToCommentsSection();
comments
.waitForElementVisible('@respectButton')
.click('@respectButton');
const profile = embed
.getProfileSection();
const profile = embedStream.goToProfileSection();
profile
.waitForElementVisible('@myCommentHistory')
@@ -130,31 +97,16 @@ module.exports = {
},
'user logs out': (client) => {
const embedStream = client.page.embedStream();
const comments = embedStream.goToCommentsSection();
const embed = embedStream
.navigate()
.getEmbedSection();
embed
.waitForElementVisible('@commentsTabButton')
.click('@commentsTabButton')
.waitForElementVisible('@logoutButton')
.click('@logoutButton');
comments
.logout();
},
'not logged in user clicks my profile tab': (client) => {
const embedStream = client.page.embedStream();
const embed = embedStream
.navigate()
.getEmbedSection();
const profile = embed
.getProfileSection();
const profile = embedStream.goToProfileSection();
profile
.assert.visible('@notLoggedIn');
},
after: (client) => {
client.end();
}
};
+117
View File
@@ -0,0 +1,117 @@
module.exports = {
before: (client) => {
client.resizeWindow(1600, 1200);
},
afterEach: (client, done) => {
if (client.currentTest.results.failed) {
throw new Error('Test Case failed, skipping all the rest');
}
done();
},
after: (client) => {
client.end();
},
'admin logs in': (client) => {
const adminPage = client.page.admin();
const {testData: {admin}} = client.globals;
adminPage.navigateAndLogin(admin);
},
'admin flags user\'s username as offensive': (client) => {
const embedStream = client.page.embedStream();
const comments = embedStream
.navigate()
.ready();
comments
.waitForElementVisible('@firstComment')
.waitForElementVisible('@flagButton')
.click('@flagButton');
comments.section.flag
.waitForElementVisible('@flagUsernameRadio')
.click('@flagUsernameRadio')
.waitForElementVisible('@continueButton')
.click('@continueButton')
.waitForElementVisible('@offensiveUsernameRadio')
.click('@offensiveUsernameRadio')
.click('@continueButton')
.waitForElementVisible('@popUpText')
.click('@continueButton');
},
'admin goes to Reported Usernames': (client) => {
const adminPage = client.page.admin();
const community = adminPage
.navigate()
.ready()
.goToCommunity();
community
.waitForElementVisible('@flaggedAccountsContainer')
.waitForElementVisible('@flaggedUser');
},
'admin rejects the user flag': (client) => {
const community = client.page.admin().section.community;
community
.waitForElementVisible('@flaggedUserRejectButton')
.click('@flaggedUserRejectButton');
},
'admin suspends the user': (client) => {
const community = client.page.admin().section.community;
community
.waitForElementVisible('@usernameDialog')
.waitForElementVisible('@usernameDialogButtons')
.waitForElementVisible('@usernameDialogSuspend')
.click('@usernameDialogSuspend')
.waitForElementVisible('@usernameDialogSuspensionMessage')
.click('@usernameDialogSuspend')
.waitForElementNotPresent('@flaggedUser');
},
'admin logs out': (client) => {
client.page.admin().logout();
},
'user logs in': (client) => {
const {testData: {user}} = client.globals;
const embedStream = client.page.embedStream();
embedStream
.navigate()
.ready()
.openLoginPopup((popup) => popup.login(user));
},
'user account is suspended, should see restricted message box': (client) => {
const embedStream = client.page.embedStream();
const comments = embedStream.section.comments;
comments
.waitForElementVisible('@restrictedMessageBox');
},
'user picks another username': (client) => {
const embedStream = client.page.embedStream();
const comments = embedStream.section.comments;
const {testData: {user}} = client.globals;
comments
.waitForElementVisible('@suspendedAccountInput')
.setValue('@suspendedAccountInput', `${user.username}_alternative`)
.waitForElementVisible('@suspendedAccountSubmitButton')
.click('@suspendedAccountSubmitButton')
.waitForElementNotPresent('@suspendedAccountInput');
},
'user should not be able to comment': (client) => {
const embedStream = client.page.embedStream();
const comments = embedStream.section.comments;
comments
.waitForElementNotPresent('@commentBoxTextarea')
.waitForElementNotPresent('@commentBoxPostButton');
},
};
+125
View File
@@ -0,0 +1,125 @@
module.exports = {
before: (client) => {
client.resizeWindow(1600, 1200);
},
afterEach: (client, done) => {
if (client.currentTest.results.failed) {
throw new Error('Test Case failed, skipping all the rest');
}
done();
},
after: (client) => {
client.end();
},
'admin logs in': (client) => {
const adminPage = client.page.admin();
const {testData: {admin}} = client.globals;
adminPage.navigateAndLogin(admin);
},
'navigate to the embed stream': (client) => {
const embedStream = client.page.embedStream();
embedStream
.navigate()
.ready();
},
'admin bans user': (client) => {
const embedStream = client.page.embedStream();
const comments = embedStream.section.comments;
comments.section.mod
.waitForElementVisible('@arrow')
.click('@arrow')
.waitForElementVisible('@menu')
.waitForElementVisible('@banButton')
.click('@banButton');
embedStream
.waitForElementVisible('@banDialog')
.waitForElementVisible('@banDialogConfirmButton')
.click('@banDialogConfirmButton')
.waitForElementNotVisible('@banDialog');
},
'admin logs out': (client) => {
const comments = client.page.embedStream().section.comments;
comments
.logout();
},
'user logs in': (client) => {
const {testData: {user}} = client.globals;
const comments = client.page.embedStream().section.comments;
comments
.openLoginPopup((popup) => popup.login(user));
},
'user account is banned, should see restricted message box': (client) => {
const embedStream = client.page.embedStream();
const comments = embedStream.section.comments;
comments
.waitForElementVisible('@restrictedMessageBox');
},
'user logs out': (client) => {
const embedStream = client.page.embedStream();
const comments = embedStream.section.comments;
comments
.logout();
},
'admin logs in (2)': (client) => {
const adminPage = client.page.admin();
const {testData: {admin}} = client.globals;
adminPage.navigateAndLogin(admin);
},
'admin goes to community': (client) => {
const adminPage = client.page.admin();
adminPage
.goToCommunity()
.goToPeople();
},
'admin removes ban from user': (client) => {
const people = client.page.admin()
.section.community
.section.people;
people
.waitForElementVisible('@firstRow')
.waitForElementVisible('@dropdownStatus')
.click('@dropdownStatus')
.waitForElementVisible('@dropdownStatusActive')
.click('@optionActive');
},
'admin logs out 2': (client) => {
client.page.admin().logout();
},
'navigate to the embed stream 2': (client) => {
const embedStream = client.page.embedStream();
embedStream
.navigate()
.ready();
},
'user logs in 2': (client) => {
const {testData: {user}} = client.globals;
const comments = client.page.embedStream().section.comments;
comments
.openLoginPopup((popup) => popup.login(user));
},
'user should be able to comment': (client) => {
const embedStream = client.page.embedStream();
const comments = embedStream.section.comments;
comments
.waitForElementVisible('@commentBoxTextarea')
.waitForElementVisible('@commentBoxPostButton');
},
};
+138
View File
@@ -0,0 +1,138 @@
module.exports = {
before: (client) => {
client.resizeWindow(1600, 1200);
},
afterEach: (client, done) => {
if (client.currentTest.results.failed) {
throw new Error('Test Case failed, skipping all the rest');
}
done();
},
after: (client) => {
client.end();
},
'user logs in': (client) => {
const {testData: {user}} = client.globals;
const embedStream = client.page.embedStream();
const comments = client.page.embedStream().section.comments;
embedStream
.navigate()
.ready();
comments
.openLoginPopup((popup) => popup.login(user));
},
'user posts comment': (client) => {
const comments = client.page.embedStream().section.comments;
const {testData: {comment}} = client.globals;
comments
.waitForElementVisible('@commentBoxTextarea')
.setValue('@commentBoxTextarea', comment.body)
.waitForElementVisible('@commentBoxPostButton')
.click('@commentBoxPostButton')
.waitForElementVisible('@firstCommentContent')
.getText('@firstCommentContent', (result) => {
comments.assert.equal(result.value, comment.body);
});
},
'user logs out': (client) => {
const embedStream = client.page.embedStream();
const comments = embedStream.section.comments;
comments
.logout();
},
'admin logs in': (client) => {
const adminPage = client.page.admin();
const {testData: {admin}} = client.globals;
adminPage.navigateAndLogin(admin);
},
'navigate to the embed stream': (client) => {
const embedStream = client.page.embedStream();
embedStream
.navigate()
.ready();
},
'admin reports comment': (client) => {
const embedStream = client.page.embedStream();
const comments = embedStream.section.comments;
comments
.waitForElementVisible('@firstComment')
.waitForElementVisible('@flagButton')
.click('@flagButton');
comments.section.flag
.waitForElementVisible('@flagCommentRadio')
.click('@flagCommentRadio')
.waitForElementVisible('@continueButton')
.click('@continueButton')
.waitForElementVisible('@spamCommentRadio')
.click('@spamCommentRadio')
.click('@continueButton')
.waitForElementVisible('@popUpText')
.click('@continueButton');
},
'admin suspends user': (client) => {
const adminPage = client.page.admin();
const moderate = adminPage.section.moderate;
adminPage
.navigate()
.ready()
.goToModerate();
moderate
.waitForElementVisible('@comment')
.waitForElementVisible('@commentActionMenu')
.waitForElementVisible('@actionMenuButton')
.click('@actionMenuButton')
.waitForElementVisible('@actionItemSuspendUser')
.click('@actionItemSuspendUser');
adminPage
.waitForElementVisible('@suspendUserDialog')
.waitForElementVisible('@suspendUserConfirmButton')
.click('@suspendUserConfirmButton')
.waitForElementVisible('@supendUserSendButton')
.click('@supendUserSendButton');
adminPage
.waitForElementVisible('@toast')
.waitForElementVisible('@toastClose')
.click('@toastClose');
},
'admin logs out': (client) => {
const adminPage = client.page.admin();
adminPage
.logout();
},
'user logs in (2)': (client) => {
const {testData: {user}} = client.globals;
const embedStream = client.page.embedStream();
const comments = client.page.embedStream().section.comments;
embedStream
.navigate()
.ready();
comments
.openLoginPopup((popup) => popup.login(user));
},
'user account is suspended, should see restricted message box': (client) => {
const embedStream = client.page.embedStream();
const comments = embedStream.section.comments;
comments
.waitForElementVisible('@restrictedMessageBox');
},
};
+41
View File
@@ -0,0 +1,41 @@
/**
* SortedWindowHandler assists in making e2e tests more robust by returning
* deterministic window handles. An instance must be created before new windows
* are created and windowHandles must be called each time a window was created or
* closed.
*/
class SortedWindowHandler {
/**
* Constructor, must be called before new windows were created.
*/
constructor(client) {
this.client = client;
this.client.windowHandles((result) => {
this.handles = result.value;
if (this.handles.length > 2) {
throw new Error('SortedWindowHandler must be created before new windows were created.');
}
});
}
/**
* windowHandles will call given `callback` with an array of window handles.
*/
windowHandles(callback) {
this.client.windowHandles((result) => {
this.handles = this.handles.filter((handle) => result.value.includes(handle));
const remaining = result.value.filter((handle) => !this.handles.includes(handle));
if (remaining.length === 1) {
this.handles.push(remaining[0]);
}
if (remaining.length > 1) {
throw new Error('Cannot detect new window handle, because more than one windows was created.');
}
callback(this.handles);
});
}
}
module.exports = SortedWindowHandler;
+20 -1
View File
@@ -1,8 +1,11 @@
const UsersService = require('../../../services/users');
const SettingsService = require('../../../services/settings');
const MailerService = require('../../../services/mailer');
const chai = require('chai');
chai.use(require('chai-as-promised'));
const sinon = require('sinon');
chai.use(require('sinon-chai'));
const expect = chai.expect;
describe('services.UsersService', () => {
@@ -15,7 +18,7 @@ describe('services.UsersService', () => {
mockUsers = await UsersService.createLocalUsers([{
email: 'stampi@gmail.com',
username: 'Stampi',
password: '1Coral!-'
password: '1Coral!-',
}, {
email: 'sockmonster@gmail.com',
username: 'Sockmonster',
@@ -25,6 +28,12 @@ describe('services.UsersService', () => {
username: 'Marvel',
password: '3Coral!3'
}]);
sinon.spy(MailerService, 'sendSimple');
});
afterEach(() => {
MailerService.sendSimple.restore();
});
describe('#findById()', () => {
@@ -149,7 +158,11 @@ describe('services.UsersService', () => {
.then(() => UsersService.findById(mockUsers[0].id))
.then((user) => {
expect(user).to.have.property('status', 'ACTIVE');
})
.then(() => {
expect(MailerService.sendSimple).to.not.have.been.called;
});
});
});
@@ -188,6 +201,12 @@ describe('services.UsersService', () => {
.then(() => UsersService.findById(mockUsers[0].id))
.then((user) => {
expect(user).to.have.property('status', 'BANNED');
})
.then(() => {
expect(MailerService.sendSimple).to.have.been.calledWithMatch({
template: 'banned',
to: mockUsers[0].profiles[0].id
});
});
});
+3 -3
View File
@@ -7953,9 +7953,9 @@ select@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d"
selenium-standalone@6.9.0:
version "6.9.0"
resolved "https://registry.yarnpkg.com/selenium-standalone/-/selenium-standalone-6.9.0.tgz#5805f403c82b48b4e136d8a97f1832aad7852bd5"
selenium-standalone@^6.11.0:
version "6.11.0"
resolved "https://registry.yarnpkg.com/selenium-standalone/-/selenium-standalone-6.11.0.tgz#eb21ff65f3b2ece75061b35d4f9e7572ac0280c2"
dependencies:
async "^2.1.4"
commander "^2.9.0"