diff --git a/client-admin/src/components/conversation-admin/comment-moderation/comment.js b/client-admin/src/components/conversation-admin/comment-moderation/comment.js index b936f78b3..848fbef7b 100644 --- a/client-admin/src/components/conversation-admin/comment-moderation/comment.js +++ b/client-admin/src/components/conversation-admin/comment-moderation/comment.js @@ -25,7 +25,7 @@ class Comment extends React.Component { render() { return ( - + {this.props.comment.active ? null : 'Comment flagged as toxic by Jigsaw Perspective API. Comment not shown to participants. Accept to override.'} {this.props.comment.txt} @@ -44,7 +44,7 @@ class Comment extends React.Component { ) : null} {this.props.rejectButton ? ( - ) : null} diff --git a/client-admin/src/components/conversation-admin/comment-moderation/index.js b/client-admin/src/components/conversation-admin/comment-moderation/index.js index 012b49804..162c37dd9 100644 --- a/client-admin/src/components/conversation-admin/comment-moderation/index.js +++ b/client-admin/src/components/conversation-admin/comment-moderation/index.js @@ -69,6 +69,7 @@ class CommentModeration extends React.Component { +
{this.props.accepted_comments !== null ? this.createCommentMarkup() : 'Loading accepted comments...'} diff --git a/client-admin/src/components/conversation-admin/comment-moderation/moderate-comments-rejected.js b/client-admin/src/components/conversation-admin/comment-moderation/moderate-comments-rejected.js index d9dd77747..922b52a4e 100644 --- a/client-admin/src/components/conversation-admin/comment-moderation/moderate-comments-rejected.js +++ b/client-admin/src/components/conversation-admin/comment-moderation/moderate-comments-rejected.js @@ -38,7 +38,7 @@ class ModerateCommentsRejected extends React.Component { render() { return ( -
+
{this.props.rejected_comments !== null ? this.createCommentMarkup() : 'Loading rejected comments...'} diff --git a/client-admin/src/components/conversation-admin/comment-moderation/moderate-comments-todo.js b/client-admin/src/components/conversation-admin/comment-moderation/moderate-comments-todo.js index 45dc29b01..3d7bb6496 100644 --- a/client-admin/src/components/conversation-admin/comment-moderation/moderate-comments-todo.js +++ b/client-admin/src/components/conversation-admin/comment-moderation/moderate-comments-todo.js @@ -48,7 +48,7 @@ class ModerateCommentsTodo extends React.Component { render() { const max = 100; return ( -
+

Displays maximum {max} comments

{this.props.unmoderated_comments !== null diff --git a/client-admin/src/components/conversation-admin/index.js b/client-admin/src/components/conversation-admin/index.js index df0ad4105..b9c1b37ff 100644 --- a/client-admin/src/components/conversation-admin/index.js +++ b/client-admin/src/components/conversation-admin/index.js @@ -75,6 +75,7 @@ class ConversationAdminContainer extends React.Component { sx={{ variant: url === 'comments' ? 'links.activeNav' : 'links.nav' }} + data-test-id="moderate-comments" to={`${match.url}/comments`}> Moderate diff --git a/client-admin/src/components/conversation-admin/report/reports-list.js b/client-admin/src/components/conversation-admin/report/reports-list.js index 04ac1d101..794f8e04a 100644 --- a/client-admin/src/components/conversation-admin/report/reports-list.js +++ b/client-admin/src/components/conversation-admin/report/reports-list.js @@ -6,6 +6,7 @@ import PropTypes from 'prop-types' import Url from '../../../util/url' import { connect } from 'react-redux' import { Heading, Box, Button } from 'theme-ui' +import { populateZidMetadataStore } from '../../../actions' import ComponentHelpers from '../../../util/component-helpers' import NoPermission from '../no-permission' @@ -31,11 +32,24 @@ class ReportsList extends React.Component { }) }) } - + componentDidMount() { const { zid_metadata } = this.props + + // eslint-disable-next-line react/prop-types + this.props.dispatch( + populateZidMetadataStore(this.props.match.params.conversation_id) + ) - if (zid_metadata.is_mod) { + // If we already have is_mod, get the data + if (zid_metadata?.is_mod) { + this.getData() + } + } + + componentDidUpdate() { + const { zid_metadata } = this.props + if (zid_metadata?.is_mod) { this.getData() } } @@ -75,7 +89,7 @@ class ReportsList extends React.Component { {this.state.reports.map((report) => { return ( - + +
Error Loading
{this.state.errorText}
@@ -455,21 +455,21 @@ class App extends React.Component { } if (this.state.nothingToShow) { return ( -
+
Nothing to show yet
); } if (this.state.loading) { return ( -
+
Loading ...
); } console.log("top level app state and props", this.state, this.props); return ( -
+
{ + cy.visit('/m/' + this.convoId) + cy.wait('@getConversations') + + // Set up initial comments + cy.get('textarea[data-test-id="seed_form"]').type('Initial comment for moderation') + cy.get('button').contains('Submit').click() + cy.wait('@createComment') + }) + }) + + describe('Basic Moderation Actions', function () { + it('should reject an approved comment', function () { + cy.get('[data-test-id="moderate-comments"]').click() + cy.get('[data-test-id="filter-approved"]').click() + cy.get('[data-test-id="reject-comment"]').click() + cy.wait('@updateComment').then(({ response }) => { + expect(response.statusCode).to.equal(200) + }) + cy.get('[data-test-id="pending-comment"]').should('not.exist') + cy.get('[data-test-id="filter-rejected"]').click() + cy.contains('button', 'accept').click() + cy.wait('@updateComment').then(({ response }) => { + expect(response.statusCode).to.equal(200) + }) + }) + }) + + describe('Moderation Settings', function () { + it('should filter comments by moderation status', function () { + cy.get('[data-test-id="moderate-comments"]').click() + + // Test different filter options + cy.get('[data-test-id="filter-approved"]').click() + cy.get('[data-test-id="approved-comments"]').should('be.visible') + + cy.get('[data-test-id="filter-rejected"]').click() + cy.get('[data-test-id="rejected-comments"]').should('exist') + + cy.get('[data-test-id="mod-queue"]').click() + cy.get('[data-test-id="pending-comment"]').should('exist') + }) + }) +}) diff --git a/e2e/cypress/e2e/client-admin/conversation.cy.js b/e2e/cypress/e2e/client-admin/conversation.cy.js index 1323d5045..f5a8f8230 100644 --- a/e2e/cypress/e2e/client-admin/conversation.cy.js +++ b/e2e/cypress/e2e/client-admin/conversation.cy.js @@ -38,29 +38,25 @@ describe('Conversation: Configure', function () { cy.contains('button', 'Create new conversation').click() cy.wait('@createConversation').then(({ response }) => - cy.location('pathname').should('eq', '/m/' + response.body.conversation_id) + cy.location('pathname').should('eq', '/m/' + response.body.conversation_id), ) cy.contains('h3', 'Configure').should('be.visible') - cy.get('input[data-test-id="topic"]') - .type('Test topic') + cy.get('input[data-test-id="topic"]').type('Test topic') - cy.get('input[data-test-id="topic"]') - .then(() => cy.focused().blur()) + cy.get('input[data-test-id="topic"]').then(() => cy.focused().blur()) cy.wait('@updateConversation').then(({ response }) => - expect(response.body.topic).to.equal('Test topic') + expect(response.body.topic).to.equal('Test topic'), ) - cy.get('textarea[data-test-id="description"]') - .type('Test description') + cy.get('textarea[data-test-id="description"]').type('Test description') - cy.get('textarea[data-test-id="description"]') - .then(() => cy.focused().blur()) + cy.get('textarea[data-test-id="description"]').then(() => cy.focused().blur()) cy.wait('@updateConversation').then(({ response }) => - expect(response.body.description).to.equal('Test description') + expect(response.body.description).to.equal('Test description'), ) cy.get('textarea[data-test-id="seed_form"]').type('Test seed comment') @@ -72,6 +68,74 @@ describe('Conversation: Configure', function () { }) }) + describe('Conversation Participation', function () { + beforeEach(function () { + cy.createConvo().then(() => { + cy.visit('/m/' + this.convoId) + cy.wait('@getConversations') + cy.get('input[data-test-id="topic"]').type('Participation Test Topic') + cy.get('textarea[data-test-id="description"]').type('Test description') + cy.get('textarea[data-test-id="seed_form"]').type('Initial seed comment') + cy.get('button').contains('Submit').click() + }) + }) + + it('should allow multiple seed comments', function () { + cy.wait('@createComment', { timeout: 7000 }) + cy.get('textarea[data-test-id="seed_form"]').clear() + cy.get('textarea[data-test-id="seed_form"]').type('Second seed comment') + // pause for 1 second to ensure the button is in correct state + cy.pause() + + cy.get('button').contains('Submit').click() + cy.wait('@createComment', { timeout: 7000 }) + cy.get('textarea[data-test-id="seed_form"]').clear() + cy.get('textarea[data-test-id="seed_form"]').type('third seed comment') + // pause for 1 second to ensure the button is in correct state + cy.pause() + + cy.get('button').contains('Submit').click() + cy.wait('@createComment', { timeout: 7000 }) + + // Verify all seed comments are visible by checking API response + cy.request(`/api/v3/comments?conversation_id=${this.convoId}`).then((response) => + expect(response.body.length).to.equal(3), + ) + }) + + it('should handle special characters in topic and description', function () { + const specialTopic = 'Test & Topic with $pecial ' + const specialDesc = '!@#$%^&*() Special description 你好' + + cy.get('input[data-test-id="topic"]').clear().type(specialTopic) + cy.get('input[data-test-id="topic"]').then(() => cy.focused().blur()) + cy.wait('@updateConversation') + + cy.get('textarea[data-test-id="description"]').clear().type(specialDesc) + cy.get('textarea[data-test-id="description"]').then(() => cy.focused().blur()) + cy.wait('@updateConversation') + + // Verify the content is saved correctly + cy.reload() + cy.get('input[data-test-id="topic"]').should('have.value', specialTopic) + cy.get('textarea[data-test-id="description"]').should('have.value', specialDesc) + }) + }) + + describe('Conversation Settings', function () { + beforeEach(function () { + cy.createConvo().then(() => cy.visit('/m/' + this.convoId)) + cy.wait('@getConversations') + }) + + it('should toggle visibility settings correctly', function () { + cy.get('input[data-test-id="vis_type"]').check() + cy.wait('@updateConversation').then(({ response }) => { + expect(response.body.is_public).to.be.true + }) + }) + }) + describe('Closing a Conversation', function () { beforeEach(function () { cy.createConvo().then(() => cy.visit('/m/' + this.convoId)) diff --git a/e2e/cypress/e2e/client-participation/social-login.cy.js b/e2e/cypress/e2e/client-participation/social-login.cy.js index 177c00e7b..f61467774 100644 --- a/e2e/cypress/e2e/client-participation/social-login.cy.js +++ b/e2e/cypress/e2e/client-participation/social-login.cy.js @@ -17,11 +17,14 @@ describe('Social login buttons', function () { }) beforeEach(function () { + cy.intercept('POST', '/api/v3/comments').as('postComment') + cy.intercept('GET', '/api/v3/math/pca2*').as('getMath') cy.intercept('GET', '/api/v3/comments*').as('getComments') cy.intercept('GET', '/api/v3/conversations*').as('getConversations') cy.intercept('GET', '/api/v3/users*').as('getUsers') cy.intercept('GET', '/api/v3/participationInit*').as('participationInit') cy.intercept('PUT', '/api/v3/conversations').as('putConversations') + cy.intercept('GET', '/api/v3/conversationStats*').as('getConversationStats') }) describe('default settings', function () { @@ -123,12 +126,17 @@ describe('Social login buttons', function () { cy.get(commentView).find(twitterCommentBtn).should('not.exist') }) - it('allows both providers to be disabled', function () { + it('allows both providers to be disabled and a user can vote and comment', function () { cy.ensureUser('moderator') cy.visit(this.adminPath) cy.get(facebookAuthOpt).uncheck() cy.get(twitterAuthOpt).uncheck() - + cy.get('input[data-test-id="auth_needed_to_write"]').uncheck() // testing to ensure commenting works + cy.get('input[data-test-id="auth_needed_to_vote"]').uncheck() // testing to ensure commenting works + // check monitor page + cy.visit(`/m${this.convoPath}/stats`) + cy.wait('@getConversationStats') + cy.contains('2 participants voted').should('be.visible') cy.ensureUser('participant5') cy.visit(this.convoPath) cy.wait('@participationInit') @@ -138,11 +146,13 @@ describe('Social login buttons', function () { cy.get(voteView).find(facebookVoteBtn).should('not.exist') cy.get(voteView).find(twitterVoteBtn).should('not.exist') - - cy.get(commentView).find('button#comment_button').click() - cy.get(commentView).find(facebookCommentBtn).should('not.exist') cy.get(commentView).find(twitterCommentBtn).should('not.exist') + cy.get('textarea#comment_form_textarea').type('This is a test comment') + cy.get('button#comment_button').click() + + cy.wait('@postComment').its('response.statusCode').should('eq', 200) + cy.contains('Statement submitted!').should('be.visible') }) }) }) diff --git a/e2e/cypress/e2e/client-participation/visualization.cy.js b/e2e/cypress/e2e/client-participation/visualization.cy.js index f13631c10..5796f9a95 100644 --- a/e2e/cypress/e2e/client-participation/visualization.cy.js +++ b/e2e/cypress/e2e/client-participation/visualization.cy.js @@ -20,6 +20,7 @@ describe('Visualization', function () { }) beforeEach(function () { + cy.intercept('POST', '/api/v3/comments').as('postComment') cy.intercept('GET', '/api/v3/votes/famous*').as('getFamous') cy.intercept('GET', '/api/v3/math/pca2*').as('getMath') cy.intercept('GET', '/api/v3/participationInit*').as('participationInit') diff --git a/e2e/cypress/e2e/client-report/reports.cy.js b/e2e/cypress/e2e/client-report/reports.cy.js new file mode 100644 index 000000000..bac730510 --- /dev/null +++ b/e2e/cypress/e2e/client-report/reports.cy.js @@ -0,0 +1,51 @@ +describe('Reports', function () { + beforeEach(function () { + // Disable matrix functionality to avoid assertExists errors + cy.intercept('GET', '/api/v3/math/pca2*', { + statusCode: 200, + body: { + 'base-clusters': [], + consensus: {}, + 'group-aware-consensus': {}, + 'group-clusters': [], + 'group-votes': [], + 'n-cmts': 0, + 'user-vote-counts': [], + 'votes-base': [], + center: [], + 'comment-extremity': [], + 'comment-projection': [], + comps: [], + repness: [], + tids: [], + pca: { + center: [], + 'comment-extremity': [], + 'comment-projection': [], + comps: [], + }, + }, // or minimal math data structure if needed + }).as('getMath') + cy.intercept('GET', '/api/v3/conversations*').as('getConversations') + cy.intercept('GET', '/api/v3/reports*').as('getReports') + cy.intercept('GET', '/api/v3/comments*').as('getComments') + + cy.createConvo().then(() => { + cy.visit('/m/' + this.convoId) + cy.wait('@getConversations') + cy.visit('/m/' + this.convoId + '/reports') + }) + }) + + describe('Reports List', function () { + it('should create a report URL and show link', function () { + // pause for ten seconds to allow the button to be visible + cy.pause() + cy.contains('button', 'Create report url').click() + cy.get('a[href*="/report/"]').then(($link) => { + cy.visit($link.attr('href')) + cy.get('[data-testid="reports-overview"]').should('exist') + }) + }) + }) +})