diff --git a/site/gatsby-site/cypress/e2e/integration/citeEdit.cy.js b/site/gatsby-site/cypress/e2e/integration/citeEdit.cy.js deleted file mode 100644 index dff0592ca8..0000000000 --- a/site/gatsby-site/cypress/e2e/integration/citeEdit.cy.js +++ /dev/null @@ -1,1228 +0,0 @@ -import { maybeIt } from '../../support/utils'; -import updateOneReport from '../../fixtures/reports/updateOneReport.json'; -import updateOneReportTranslation from '../../fixtures/reports/updateOneReportTranslation.json'; -import { format, getUnixTime } from 'date-fns'; -import reportWithTranslations from '../../fixtures/reports/reportWithTranslations.json'; -import issueWithTranslations from '../../fixtures/reports/issueWithTranslations.json'; -import report10 from '../../fixtures/reports/report.json'; -const { gql } = require('@apollo/client'); - -describe('Edit report', () => { - const url = '/cite/edit?report_number=10'; - - let user; - - before('before', () => { - cy.query({ - query: gql` - { - user(query: { first_name: "Test", last_name: "User" }) { - userId - first_name - last_name - } - } - `, - }).then(({ data: { user: userData } }) => { - user = userData; - }); - }); - - it('Successfully loads', () => { - cy.visit(url); - - cy.disableSmoothScroll(); - }); - - maybeIt('Should load and update report values', () => { - cy.login(Cypress.env('e2eUsername'), Cypress.env('e2ePassword')); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'FindReportWithTranslations', - 'FindReportWithTranslations', - reportWithTranslations - ); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'FindReport', - 'FindReport', - report10 - ); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'FindIncidents', - 'FindIncidents', - { - data: { - incidents: [ - { - _typename: 'Incident', - incident_id: 1, - title: 'Incident 1', - }, - ], - }, - } - ); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'FindIncidentsTitles', - 'FindIncidentsTitles', - { - data: { - incidents: [ - { - _typename: 'Incident', - incident_id: 1, - title: 'Incident 1', - }, - { - _typename: 'Incident', - incident_id: 2, - title: 'Incident 2', - }, - ], - }, - } - ); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'logReportHistory', - 'logReportHistory', - { - data: { - logReportHistory: { - report_number: 10, - }, - }, - } - ); - - cy.visit(url); - - cy.wait(['@FindReportWithTranslations', '@FindIncidents', '@FindReport']); - - [ - 'authors', - 'date_downloaded', - 'date_published', - 'image_url', - 'submitters', - 'title', - 'editor_notes', - ].forEach((key) => { - cy.get(`[name=${key}]`).should( - 'have.value', - reportWithTranslations.data.report[key].toString() - ); - }); - - cy.getEditorText().should('eq', reportWithTranslations.data.report.text); - - cy.contains('label', 'Incident IDs') - .next() - .contains('[data-cy="token"]', 'Incident 1') - .should('be.visible'); - - cy.get('.submit-report-tags [option="Test Tag"]').should('have.length', 1); - - cy.get('[data-cy="translation-es"] [type="text"]').should( - 'have.value', - reportWithTranslations.data.report.translations_es.title - ); - - cy.getEditorText('[data-cy="translation-es"] .CodeMirror').should( - 'eq', - reportWithTranslations.data.report.translations_es.text - ); - - const updates = { - authors: 'Test Author', - date_downloaded: '2022-01-01', - date_published: '2022-02-02', - image_url: 'https://test.com/test.jpg', - submitters: 'Test Submitter', - title: 'Test Title', - url: 'https://www.test.com/test', - editor_notes: 'Pro iustitia tantum', - }; - - Object.keys(updates).forEach((key) => { - cy.get(`[name=${key}]`).clear().type(updates[key]); - }); - - cy.get(`[name="quiet"]`).click(); - - cy.setEditorText( - '## This is text in English\n\nthat is longer that eighty characters, yes eighty characters!', - '[data-cy="text"] .CodeMirror' - ); - - cy.get('[id^=submit-report-tags]').type('New Tag'); - - cy.get('a[aria-label="New Tag"]').click(); - - cy.get('[data-cy="translation-es"] [type="text"]') - .clear() - .type('Este es un titulo en Espanol!'); - - cy.setEditorText( - '## Este es texto en espanol\n\nque es mas largo que ochenta caracters, si ochenta caracteres!', - '[data-cy="translation-es"] .CodeMirror' - ); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'UpdateReport', - 'updateReport', - updateOneReport - ); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'UpdateReportTranslation', - 'updateOneReportTranslation', - updateOneReportTranslation - ); - - const now = new Date(); - - cy.clock(now); - - cy.contains('button', 'Submit').click(); - - const expectedReport = { - authors: ['Test Author'], - cloudinary_id: 'reports/test.com/test.jpg', - date_downloaded: new Date('2022-01-01').toISOString(), - date_modified: format(now, 'yyyy-MM-dd'), - date_published: new Date('2022-02-02').toISOString(), - epoch_date_modified: getUnixTime(now), - epoch_date_published: 1643760000, - flag: null, - image_url: 'https://test.com/test.jpg', - report_number: 10, - submitters: ['Test Submitter'], - tags: ['Test Tag', 'New Tag'], - text: '## This is text in English\n\nthat is longer that eighty characters, yes eighty characters!', - plain_text: - 'This is text in English\n\nthat is longer that eighty characters, yes eighty characters!\n', - title: 'Test Title', - url: 'https://www.test.com/test', - source_domain: 'test.com', - editor_notes: 'Pro iustitia tantum', - language: 'en', - quiet: true, - }; - - cy.wait('@updateReport').then((xhr) => { - expect(xhr.request.body.variables.query.report_number).eq(expectedReport.report_number); - expect({ - ...xhr.request.body.variables.set, - date_modified: format(new Date(xhr.request.body.variables.set.date_modified), 'yyyy-MM-dd'), - }).to.deep.eq(expectedReport); - }); - - cy.wait('@logReportHistory') - .its('request.body.variables.input') - .then((input) => { - const expectedResult = { - ...report10.data.report, - ...expectedReport, - modifiedBy: user.userId, - user: report10.data.report.user.userId, - date_modified: input.date_modified, - }; - - expect(input).to.deep.eq(expectedResult); - }); - - cy.wait('@updateOneReportTranslation').then((xhr) => { - expect(xhr.request.body.variables.input.language).eq('es'); - expect(xhr.request.body.variables.input.report_number).eq(10); - expect(xhr.request.body.variables.input.text).eq( - '## Este es texto en espanol\n\nque es mas largo que ochenta caracters, si ochenta caracteres!' - ); - expect(xhr.request.body.variables.input.plain_text).eq( - 'Este es texto en espanol\n\nque es mas largo que ochenta caracters, si ochenta caracteres!\n' - ); - expect(xhr.request.body.variables.input.title).eq('Este es un titulo en Espanol!'); - }); - - cy.get('.tw-toast').contains('Incident report 10 updated successfully.').should('exist'); - }); - - maybeIt('Should load and update Issue values', () => { - cy.login(Cypress.env('e2eUsername'), Cypress.env('e2ePassword')); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'FindReportWithTranslations', - 'FindReportWithTranslations', - issueWithTranslations - ); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'FindReport', - 'FindReport', - report10 - ); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'FindIncidents', - 'FindIncidents', - { - data: { - incidents: [], - }, - } - ); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'FindIncidentsTitles', - 'FindIncidentsTitles', - { - data: { - incidents: [ - { - _typename: 'Incident', - incident_id: 1, - title: 'Incident 1', - }, - ], - }, - } - ); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'logReportHistory', - 'logReportHistory', - { - data: { - logReportHistory: { - report_number: 10, - }, - }, - } - ); - - cy.visit(url); - - cy.wait(['@FindReportWithTranslations', '@FindIncidents', '@FindReport']); - - [ - 'authors', - 'date_downloaded', - 'date_published', - 'image_url', - 'submitters', - 'title', - 'editor_notes', - ].forEach((key) => { - cy.get(`[name=${key}]`).should( - 'have.value', - reportWithTranslations.data.report[key].toString() - ); - }); - - cy.getEditorText().should('eq', reportWithTranslations.data.report.text); - - cy.get(`[name="incident_id"]`).should('not.exist'); - - cy.get('.submit-report-tags [option="Test Tag"]').should('have.length', 1); - - cy.get('[data-cy="translation-es"] [type="text"]').should( - 'have.value', - reportWithTranslations.data.report.translations_es.title - ); - - cy.getEditorText('[data-cy="translation-es"] .CodeMirror').should( - 'eq', - reportWithTranslations.data.report.translations_es.text - ); - - const updates = { - authors: 'Test Author', - date_downloaded: '2022-01-01', - date_published: '2022-02-02', - image_url: 'https://test.com/test.jpg', - submitters: 'Test Submitter', - title: 'Test Title', - url: 'https://www.test.com/test', - editor_notes: 'Pro iustitia tantum', - }; - - Object.keys(updates).forEach((key) => { - cy.get(`[name=${key}]`).clear().type(updates[key]); - }); - - cy.setEditorText( - '## This is text in English\n\nthat is longer that eighty characters, yes eighty characters!', - '[data-cy="text"] .CodeMirror' - ); - - cy.get('[id^=submit-report-tags]').type('New Tag'); - - cy.get('a[aria-label="New Tag"]').click(); - - cy.get('[data-cy="translation-es"] [type="text"]') - .clear() - .type('Este es un titulo en Espanol!'); - - cy.setEditorText( - '## Este es texto en espanol\n\nque es mas largo que ochenta caracters, si ochenta caracteres!', - '[data-cy="translation-es"] .CodeMirror' - ); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'UpdateReport', - 'updateReport', - updateOneReport - ); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'UpdateReportTranslation', - 'updateOneReportTranslation', - updateOneReportTranslation - ); - - const now = new Date(); - - cy.clock(now); - - cy.contains('button', 'Submit').click(); - - const expectedReport = { - authors: ['Test Author'], - cloudinary_id: 'reports/test.com/test.jpg', - date_downloaded: new Date('2022-01-01').toISOString(), - date_modified: format(now, 'yyyy-MM-dd'), - date_published: new Date('2022-02-02').toISOString(), - epoch_date_modified: getUnixTime(now), - epoch_date_published: 1643760000, - flag: null, - image_url: 'https://test.com/test.jpg', - report_number: 10, - submitters: ['Test Submitter'], - tags: ['Test Tag', 'New Tag'], - text: '## This is text in English\n\nthat is longer that eighty characters, yes eighty characters!', - plain_text: - 'This is text in English\n\nthat is longer that eighty characters, yes eighty characters!\n', - title: 'Test Title', - url: 'https://www.test.com/test', - source_domain: 'test.com', - editor_notes: 'Pro iustitia tantum', - language: 'en', - }; - - cy.wait('@updateReport').then((xhr) => { - expect(xhr.request.body.variables.query.report_number).eq(10); - expect({ - ...xhr.request.body.variables.set, - date_modified: format(new Date(xhr.request.body.variables.set.date_modified), 'yyyy-MM-dd'), - }).to.deep.eq(expectedReport); - }); - - cy.wait('@logReportHistory') - .its('request.body.variables.input') - .then((input) => { - const expectedResult = { - ...report10.data.report, - ...expectedReport, - modifiedBy: user.userId, - user: report10.data.report.user.userId, - date_modified: input.date_modified, - }; - - expect(input).to.deep.eq(expectedResult); - }); - - cy.wait('@updateOneReportTranslation').then((xhr) => { - expect(xhr.request.body.variables.input.language).eq('es'); - expect(xhr.request.body.variables.input.report_number).eq(10); - expect(xhr.request.body.variables.input.text).eq( - '## Este es texto en espanol\n\nque es mas largo que ochenta caracters, si ochenta caracteres!' - ); - expect(xhr.request.body.variables.input.plain_text).eq( - 'Este es texto en espanol\n\nque es mas largo que ochenta caracters, si ochenta caracteres!\n' - ); - expect(xhr.request.body.variables.input.title).eq('Este es un titulo en Espanol!'); - }); - - cy.contains('[data-cy="toast"]', 'Issue 10 updated successfully').should('exist'); - }); - - maybeIt('Should delete incident report', () => { - cy.login(Cypress.env('e2eUsername'), Cypress.env('e2ePassword')); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'FindReportWithTranslations', - 'FindReportWithTranslations', - issueWithTranslations - ); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'FindIncidents', - 'FindIncidents', - { - data: { - incidents: [], - }, - } - ); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'FindIncidentsTitles', - 'FindIncidentsTitles', - { - data: { - incidents: [ - { - _typename: 'Incident', - incident_id: 1, - title: 'Incident 1', - }, - ], - }, - } - ); - - cy.visit(url); - - cy.wait(['@FindIncidents', '@FindIncidentsTitles', '@FindReportWithTranslations']); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'DeleteOneReport', - 'delete', - { data: { deleteOneReport: { __typename: 'Report', report_number: 10 } } } - ); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'LinkReportsToIncidents', - 'LinkReportsToIncidents', - { - data: { - linkReportsToIncidents: [], - }, - } - ); - - cy.contains('button', 'Delete this report').click(); - - cy.wait('@delete').then((xhr) => { - expect(xhr.request.body.variables.query).to.deep.eq({ report_number: 10 }); - }); - - cy.wait('@LinkReportsToIncidents').then((xhr) => { - expect(xhr.request.body.variables.input).to.deep.eq({ - incident_ids: [], - report_numbers: [10], - }); - }); - - cy.contains('[data-cy="toast"]', 'Incident report 10 deleted successfully').should('exist'); - }); - - maybeIt('Should link a report to another incident', () => { - cy.login(Cypress.env('e2eUsername'), Cypress.env('e2ePassword')); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'FindReport', - 'FindReport', - report10 - ); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'ProbablyRelatedReports', - 'ProbablyRelatedReports', - { - data: { reports: [] }, - } - ); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'ProbablyRelatedIncidents', - 'ProbablyRelatedIncidents', - { - data: { incidents: [] }, - } - ); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'FindReportWithTranslations', - 'FindReportWithTranslations', - reportWithTranslations - ); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'FindIncidents', - 'FindIncidents', - { - data: { - incidents: [ - { - __typename: 'Incident', - incident_id: 1, - title: 'Incident 1', - }, - ], - }, - } - ); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'FindIncidentsTitles', - 'FindIncidentsTitles', - { - data: { - incidents: [ - { - _typename: 'Incident', - incident_id: 1, - title: 'Incident 1', - }, - { - _typename: 'Incident', - incident_id: 2, - title: 'Incident 2', - }, - ], - }, - } - ); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'logReportHistory', - 'logReportHistory', - { - data: { - logReportHistory: { - report_number: 10, - }, - }, - } - ); - - cy.visit(`/cite/edit?report_number=23`); - - cy.wait(['@FindReportWithTranslations', '@FindIncidents', '@FindIncidentsTitles']); - - cy.get('form[data-cy="report"]').should('be.visible'); - - cy.contains('div', 'Incident 1').next().click(); - - cy.get('[name="incident_ids"]').type('2'); - - cy.get('[id="incident_ids-item-0"]').click(); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'UpdateReportTranslation', - 'updateOneReportTranslation', - updateOneReportTranslation - ); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'UpdateReport', - 'UpdateReport', - updateOneReport - ); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'LinkReportsToIncidents', - 'LinkReportsToIncidents', - { - data: { - linkReportsToIncidents: [], - }, - } - ); - - const now = new Date(); - - cy.clock(now); - - cy.contains('button', 'Submit').click(); - - const expectedReport = { - authors: ['Marco Acevedo'], - cloudinary_id: - 'reports/assets.change.org/photos/0/yb/id/eYyBIdJOMHpqcty-1600x900-noPad.jpg?1523726975', - date_downloaded: new Date('2019-04-13').toISOString(), - date_modified: format(now, 'yyyy-MM-dd'), - date_published: new Date('2015-07-11').toISOString(), - editor_notes: '', - epoch_date_modified: getUnixTime(now), - epoch_date_published: 1436572800, - flag: null, - image_url: - 'https://assets.change.org/photos/0/yb/id/eYyBIdJOMHpqcty-1600x900-noPad.jpg?1523726975', - language: 'en', - plain_text: - 'Video still of a reproduced version of Minnie Mouse\n\nWhich appeared on the now-suspended Simple Fun channel Simple Fun.\n', - report_number: 10, - source_domain: 'change.org', - submitters: ['Roman Yampolskiy'], - tags: ['Test Tag'], - text: '## Video still of a reproduced version of Minnie Mouse\n\nWhich appeared on the now-suspended Simple Fun channel Simple Fun.', - title: 'Remove YouTube Kids app until it eliminates its inappropriate content', - url: 'https://www.change.org/p/remove-youtube-kids-app-until-it-eliminates-its-inappropriate-content', - }; - - cy.wait('@UpdateReport') - .its('request.body.variables') - .then((variables) => { - expect(variables.query.report_number).to.equal(23); - expect({ - ...variables.set, - date_modified: format(new Date(variables.set.date_modified), 'yyyy-MM-dd'), - }).deep.eq(expectedReport); - }); - - cy.wait('@logReportHistory') - .its('request.body.variables.input') - .then((input) => { - const expectedResult = { - ...report10.data.report, - ...expectedReport, - modifiedBy: user.userId, - user: report10.data.report.user.userId, - date_modified: input.date_modified, - }; - - expect(input).to.deep.eq(expectedResult); - }); - - cy.wait('@updateOneReportTranslation') - .its('request.body.variables') - .then((variables) => { - expect(variables.input.title).to.eq('Este es el Título en español'); - expect(variables.input.text).to.eq( - 'Este es un texto de prueba que tiene un largo mayor a ochenta caracteres (en español)' - ); - expect(variables.input.language).to.eq('es'); - expect(variables.input.report_number).to.eq(23); - expect(variables.input.plain_text).to.eq( - 'Este es un texto de prueba que tiene un largo mayor a ochenta caracteres (en español)\n' - ); - }); - - cy.wait('@updateOneReportTranslation') - .its('request.body.variables') - .then((variables) => { - expect(variables.input.title).to.eq(`C'est le Titre en français`); - expect(variables.input.text).to.eq( - `Il s'agit d'un texte de test de plus de quatre-vingts caractères - lorem ipsum (en français)` - ); - expect(variables.input.language).to.eq('fr'); - expect(variables.input.report_number).to.eq(23); - expect(variables.input.plain_text).to.eq( - `Il s'agit d'un texte de test de plus de quatre-vingts caractères - lorem ipsum (en français)\n` - ); - }); - - cy.wait('@updateOneReportTranslation') - .its('request.body.variables') - .then((variables) => { - expect(variables.input.title).to.eq('これは日本語でのタイトルです'); - expect(variables.input.text).to.eq( - '解サオライ協立なーづ民手ぶみドに即記朝ぐ奥置ぱで地更トるあて栄厚ぜづを祭屋ん来派どてゃ読速ヘ誌約カタシネ原39業理る。外ヒヱフ社第むせゆ由更混ソエ夕野しりすよ顔飛リの兆基う公言や置17謝后嘘5供フキヌア星集ヘラ辞勘壇崇さびわ。(日本語で)' - ); - expect(variables.input.language).to.eq('ja'); - expect(variables.input.report_number).to.eq(23); - expect(variables.input.plain_text).to.eq( - '解サオライ協立なーづ民手ぶみドに即記朝ぐ奥置ぱで地更トるあて栄厚ぜづを祭屋ん来派どてゃ読速ヘ誌約カタシネ原39業理る。外ヒヱフ社第むせゆ由更混ソエ夕野しりすよ顔飛リの兆基う公言や置17謝后嘘5供フキヌア星集ヘラ辞勘壇崇さびわ。(日本語で)\n' - ); - }); - - cy.wait('@LinkReportsToIncidents').then((xhr) => { - expect(xhr.request.body.variables.input).to.deep.eq({ - incident_ids: [2], - report_numbers: [23], - }); - }); - - cy.contains('[data-cy="toast"]', 'Incident report 23 updated successfully', { timeout: 8000 }); - }); - - maybeIt('Should display an error message if data is missing', () => { - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'FindReportWithTranslations', - 'findReportWithTranslations', - reportWithTranslations - ); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'ProbablyRelatedReports', - 'ProbablyRelatedReports', - { - data: { reports: [] }, - } - ); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'ProbablyRelatedIncidents', - 'ProbablyRelatedIncidents', - { - data: { incidents: [] }, - } - ); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'FindIncidents', - 'FindIncidents', - { data: { incidents: [] } } - ); - - cy.visit(`/cite/edit?report_number=23`); - - cy.wait('@findReportWithTranslations'); - - cy.get('form[data-cy="report"]').should('be.visible'); - - cy.get('[name="title"]').clear(); - - cy.contains('Please review report. Some data is missing.').should('exist'); - - cy.contains('button', 'Submit').should('be.disabled'); - - cy.get('[name="title"]').type( - 'Remove YouTube Kids app until it eliminates its inappropriate content' - ); - - cy.contains('button', 'Submit').should('not.be.disabled'); - }); - - maybeIt('Should convert an issue to a incident report', () => { - cy.login(Cypress.env('e2eUsername'), Cypress.env('e2ePassword')); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'FindReport', - 'FindReport', - report10 - ); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'ProbablyRelatedReports', - 'ProbablyRelatedReports', - { - data: { reports: [] }, - } - ); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'ProbablyRelatedIncidents', - 'ProbablyRelatedIncidents', - { - data: { incidents: [] }, - } - ); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'FindReportWithTranslations', - 'FindReportWithTranslations', - issueWithTranslations - ); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'FindIncidents', - 'FindIncidents', - { data: { incidents: [] } } - ); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'FindIncidentsTitles', - 'FindIncidentsTitles', - { - data: { - incidents: [ - { - _typename: 'Incident', - incident_id: 1, - title: 'Incident 1', - }, - { - _typename: 'Incident', - incident_id: 2, - title: 'Incident 2', - }, - ], - }, - } - ); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'logReportHistory', - 'logReportHistory', - { - data: { - logReportHistory: { - report_number: 10, - }, - }, - } - ); - - cy.visit(`/cite/edit?report_number=23`); - - cy.wait('@FindIncidents'); - - cy.wait('@FindReportWithTranslations'); - - cy.wait('@FindIncidentsTitles'); - - cy.get('form[data-cy="report"]').should('be.visible'); - - cy.get('[name="incident_ids"]').type('1'); - - cy.get('[id="incident_ids-item-0"]').click(); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'UpdateReport', - 'UpdateReport', - updateOneReport - ); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'UpdateReportTranslation', - 'UpdateReportTranslation', - updateOneReportTranslation - ); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'LinkReportsToIncidents', - 'LinkReportsToIncidents', - { - data: { - linkReportsToIncidents: [ - { - __typename: 'Incident', - incident_id: 1, - reports: [{ __typename: 'Report', report_number: 23 }], - }, - ], - }, - } - ); - - cy.window().then((win) => cy.stub(win, 'confirm').as('confirm').returns(true)); - - const now = new Date(); - - cy.clock(now); - - cy.contains('button', 'Submit').click(); - - cy.get('@confirm').should('have.been.calledOnce').invoke('restore'); - - const expectedReport = { - authors: ['Marco Acevedo'], - cloudinary_id: - 'reports/assets.change.org/photos/0/yb/id/eYyBIdJOMHpqcty-1600x900-noPad.jpg?1523726975', - date_downloaded: new Date('2019-04-13').toISOString(), - date_published: new Date('2015-07-11').toISOString(), - flag: null, - image_url: - 'https://assets.change.org/photos/0/yb/id/eYyBIdJOMHpqcty-1600x900-noPad.jpg?1523726975', - report_number: 10, - submitters: ['Roman Yampolskiy'], - tags: ['Test Tag'], - text: '## Video still of a reproduced version of Minnie Mouse\n\nWhich appeared on the now-suspended Simple Fun channel Simple Fun.', - plain_text: - 'Video still of a reproduced version of Minnie Mouse\n\nWhich appeared on the now-suspended Simple Fun channel Simple Fun.\n', - title: 'Remove YouTube Kids app until it eliminates its inappropriate content', - url: 'https://www.change.org/p/remove-youtube-kids-app-until-it-eliminates-its-inappropriate-content', - editor_notes: '', - language: 'en', - source_domain: 'change.org', - epoch_date_published: 1436572800, - date_modified: format(now, 'yyyy-MM-dd'), - epoch_date_modified: getUnixTime(now), - }; - - cy.wait('@UpdateReport') - .its('request.body.variables') - .then((variables) => { - expect(variables.query.report_number).to.equal(23); - expect({ - ...variables.set, - date_modified: format(new Date(variables.set.date_modified), 'yyyy-MM-dd'), - }).deep.eq(expectedReport); - }); - - cy.wait('@logReportHistory') - .its('request.body.variables.input') - .then((input) => { - const expectedResult = { - ...report10.data.report, - ...expectedReport, - modifiedBy: user.userId, - user: report10.data.report.user.userId, - date_modified: input.date_modified, - }; - - expect(input).to.deep.eq(expectedResult); - }); - - cy.wait('@UpdateReportTranslation'); - - cy.wait('@UpdateReportTranslation'); - - cy.wait('@LinkReportsToIncidents').then((xhr) => { - expect(xhr.request.body.variables.input).to.deep.eq({ - incident_ids: [1], - report_numbers: [23], - }); - }); - - cy.contains('[data-cy="toast"]', 'Incident report 23 updated successfully.', { timeout: 8000 }); - }); - - maybeIt('Should convert an incident report to an issue', () => { - cy.login(Cypress.env('e2eUsername'), Cypress.env('e2ePassword')); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'FindReport', - 'FindReport', - report10 - ); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'ProbablyRelatedReports', - 'ProbablyRelatedReports', - { - data: { reports: [] }, - } - ); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'ProbablyRelatedIncidents', - 'ProbablyRelatedIncidents', - { - data: { incidents: [] }, - } - ); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'FindReportWithTranslations', - 'FindReportWithTranslations', - reportWithTranslations - ); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'FindIncidents', - 'FindIncidents', - { - data: { - incidents: [ - { - _typename: 'Incident', - incident_id: 1, - title: 'Incident 1', - }, - ], - }, - } - ); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'FindIncidentsTitles', - 'FindIncidentsTitles', - { - data: { - incidents: [ - { - _typename: 'Incident', - incident_id: 1, - title: 'Incident 1', - }, - { - _typename: 'Incident', - incident_id: 2, - title: 'Incident 2', - }, - ], - }, - } - ); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'logReportHistory', - 'logReportHistory', - { - data: { - logReportHistory: { - report_number: 10, - }, - }, - } - ); - - cy.visit(`/cite/edit?report_number=23`); - - cy.wait('@FindIncidents'); - - cy.wait('@FindReportWithTranslations'); - - cy.wait('@FindIncidentsTitles'); - - cy.get('form[data-cy="report"]').should('be.visible'); - - cy.contains('div', 'Incident 1').next().click(); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'UpdateReport', - 'UpdateReport', - updateOneReport - ); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'UpdateReportTranslation', - 'UpdateReportTranslation', - updateOneReportTranslation - ); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'LinkReportsToIncidents', - 'LinkReportsToIncidents', - { - data: { - linkReportsToIncidents: [], - }, - } - ); - - cy.window().then((win) => cy.stub(win, 'confirm').as('confirm').returns(true)); - - const now = new Date(); - - cy.clock(now); - - cy.contains('button', 'Submit').click(); - - cy.get('@confirm').should('have.been.calledOnce').invoke('restore'); - - const expectedReport = { - authors: ['Marco Acevedo'], - cloudinary_id: - 'reports/assets.change.org/photos/0/yb/id/eYyBIdJOMHpqcty-1600x900-noPad.jpg?1523726975', - date_downloaded: new Date('2019-04-13').toISOString(), - date_published: new Date('2015-07-11').toISOString(), - flag: null, - image_url: - 'https://assets.change.org/photos/0/yb/id/eYyBIdJOMHpqcty-1600x900-noPad.jpg?1523726975', - report_number: 10, - submitters: ['Roman Yampolskiy'], - tags: ['Test Tag'], - text: '## Video still of a reproduced version of Minnie Mouse\n\nWhich appeared on the now-suspended Simple Fun channel Simple Fun.', - plain_text: - 'Video still of a reproduced version of Minnie Mouse\n\nWhich appeared on the now-suspended Simple Fun channel Simple Fun.\n', - title: 'Remove YouTube Kids app until it eliminates its inappropriate content', - url: 'https://www.change.org/p/remove-youtube-kids-app-until-it-eliminates-its-inappropriate-content', - editor_notes: '', - language: 'en', - source_domain: 'change.org', - epoch_date_published: 1436572800, - date_modified: format(now, 'yyyy-MM-dd'), - epoch_date_modified: getUnixTime(now), - }; - - cy.wait('@UpdateReport') - .its('request.body.variables') - .then((variables) => { - expect(variables.query.report_number).to.equal(23); - expect({ - ...variables.set, - date_modified: format(new Date(variables.set.date_modified), 'yyyy-MM-dd'), - }).deep.eq(expectedReport); - }); - - cy.wait('@logReportHistory') - .its('request.body.variables.input') - .then((input) => { - const expectedResult = { - ...report10.data.report, - ...expectedReport, - modifiedBy: user.userId, - user: report10.data.report.user.userId, - date_modified: input.date_modified, - }; - - expect(input).to.deep.eq(expectedResult); - }); - - cy.wait('@UpdateReportTranslation'); - - cy.wait('@UpdateReportTranslation'); - - cy.wait('@LinkReportsToIncidents').then((xhr) => { - expect(xhr.request.body.variables.input).to.deep.eq({ - incident_ids: [], - report_numbers: [23], - }); - }); - - cy.contains('[data-cy="toast"]', 'Issue 23 updated successfully', { timeout: 8000 }); - }); - - it.skip('Should display the report image', () => { - cy.login(Cypress.env('e2eUsername'), Cypress.env('e2ePassword')); - - cy.visit(url); - - cy.get('[data-cy="image-preview-figure"] img', { timeout: 15000 }).should( - 'have.attr', - 'src', - 'https://res.cloudinary.com/pai/image/upload/d_fallback.jpg/f_auto/q_auto/v1/reports/assets.change.org/photos/0/yb/id/eYyBIdJOMHpqcty-1600x900-noPad.jpg?1523726975' - ); - }); -}); diff --git a/site/gatsby-site/cypress/e2e/unit/Translator.cy.js b/site/gatsby-site/cypress/e2e/unit/Translator.cy.js deleted file mode 100644 index fb19d6ce0d..0000000000 --- a/site/gatsby-site/cypress/e2e/unit/Translator.cy.js +++ /dev/null @@ -1,224 +0,0 @@ -const { ObjectId } = require('bson'); - -const Translator = require('../../../src/utils/Translator'); - -const reports = [ - { - _id: new ObjectId('60dd465f80935bc89e6f9b01'), - authors: ['Alistair Barr'], - date_downloaded: '2019-04-13', - date_modified: '2020-06-14', - date_published: '2015-05-19', - date_submitted: '2019-06-01', - description: 'Description of report 1', - epoch_date_downloaded: 1555113600, - epoch_date_modified: 1592092800, - epoch_date_published: 1431993600, - epoch_date_submitted: 1559347200, - image_url: 'http://url.com', - language: 'en', - report_number: 1, - source_domain: 'blogs.wsj.com', - submitters: ['Roman Yampolskiy'], - tags: [], - text: 'Report 1 **text**', - plain_text: 'Report 1 text', - title: 'Report 1 title', - url: 'https://url.com/stuff', - }, - { - _id: new ObjectId('60dd465f80935bc89e6f9b02'), - authors: ['Alistair Barr'], - date_downloaded: '2019-04-13', - date_modified: '2020-06-14', - date_published: '2015-05-19', - date_submitted: '2019-06-01', - description: 'Description of report 2', - epoch_date_downloaded: 1555113600, - epoch_date_modified: 1592092800, - epoch_date_published: 1431993600, - epoch_date_submitted: 1559347200, - image_url: 'http://url.com', - language: 'es', - report_number: 2, - source_domain: 'blogs.wsj.com', - submitters: ['Roman Yampolskiy'], - tags: [], - text: 'Report 2 **text**', - plain_text: 'Report 2 text', - title: 'Report 2 title', - url: 'https://url.com/stuff', - }, -]; - -describe('Translations', () => { - it('Should translate languages only if report language differs from target language', () => { - const translatedReportsEN = [ - { - _id: '61d5ad9f102e6e30fca90ddf', - text: 'translated-en-text report 1', - title: 'translated-en-title report 1', - report_number: 1, - }, - ]; - - const translatedReportsES = [ - { - _id: '61d5ad9f102e6e30fca90ddf', - text: 'translated-es-text report 2', - title: 'translated-es-title report 2', - report_number: 2, - }, - ]; - - const reporter = { log: cy.stub() }; - - const reportsCollection = { - find: cy.stub().returns({ - toArray: cy.stub().resolves(reports), - }), - }; - - const reportsENCollection = { - find: cy.stub().returns({ - toArray: cy.stub().resolves(translatedReportsEN), - }), - insertMany: cy.stub().log(true).resolves({ insertedCount: 1 }), - }; - - const reportsESCollection = { - find: cy.stub().returns({ - toArray: cy.stub().resolves(translatedReportsES), - }), - insertMany: cy.stub().resolves({ insertedCount: 1 }), - }; - - const mongoClient = { - connect: cy.stub(), - close: cy.stub(), - db: cy.stub().returns({ - collection: (() => { - const stub = cy.stub(); - - stub.withArgs('reports').returns(reportsCollection); - stub.withArgs('reports_en').returns(reportsENCollection); - stub.withArgs('reports_es').returns(reportsESCollection); - - return stub; - })(), - }), - }; - - const translateClient = { - translate: cy.stub().callsFake((payload, { to }) => [payload.map((p) => `test-${to}-${p}`)]), - }; - - const translator = new Translator({ - mongoClient, - translateClient, - languages: [{ code: 'es' }, { code: 'en' }], - reporter, - dryRun: false, - }); - - cy.wrap(translator.run()).then(() => { - expect(mongoClient.connect.callCount).to.eq(1); - - expect(reportsENCollection.insertMany.callCount).to.eq(1); - - expect(reportsENCollection.insertMany.firstCall.args[0][0]).to.deep.equal({ - report_number: 2, - text: 'test-en-Report 2 **text**', - title: 'test-en-Report 2 title', - plain_text: 'test-en-Report 2 text\n', - }); - - expect(reportsESCollection.insertMany.callCount).to.eq(1); - - expect(reportsESCollection.insertMany.firstCall.args[0][0]).to.deep.equal({ - report_number: 1, - text: 'test-es-Report 1 **text**', - title: 'test-es-Report 1 title', - plain_text: 'test-es-Report 1 text\n', - }); - - expect(mongoClient.close.callCount).to.eq(1); - }); - }); - - it("Shouldn't call Google's translate api if dryRun is true", () => { - const translatedReportsEN = [ - { - _id: '61d5ad9f102e6e30fca90ddf', - text: 'translated-en-text report 1', - title: 'translated-en-title report 1', - report_number: 1, - }, - ]; - - const translatedReportsES = [ - { - _id: '61d5ad9f102e6e30fca90ddf', - text: 'translated-es-text report 2', - title: 'translated-es-title report 2', - report_number: 2, - }, - ]; - - const reporter = { log: cy.stub() }; - - const reportsCollection = { - find: cy.stub().returns({ - toArray: cy.stub().resolves(reports), - }), - }; - - const reportsENCollection = { - find: cy.stub().returns({ - toArray: cy.stub().resolves(translatedReportsEN), - }), - insertMany: cy.stub().resolves({ insertedCount: 1 }), - }; - - const reportsESCollection = { - find: cy.stub().returns({ - toArray: cy.stub().resolves(translatedReportsES), - }), - insertMany: cy.stub().resolves({ insertedCount: 1 }), - }; - - const mongoClient = { - connect: cy.stub(), - close: cy.stub(), - db: cy.stub().returns({ - collection: (() => { - const stub = cy.stub(); - - stub.withArgs('reports').returns(reportsCollection); - stub.withArgs('reports_en').returns(reportsENCollection); - stub.withArgs('reports_es').returns(reportsESCollection); - - return stub; - })(), - }), - }; - - const translateClient = { - translate: cy - .stub() - .callsFake((payload, { to }) => [payload.map((p) => `translated-${to}-${p}`)]), - }; - - const translator = new Translator({ - mongoClient, - translateClient, - languages: [{ code: 'es' }, { code: 'en' }], - reporter, - dryRun: true, - }); - - cy.wrap(translator.run()).then(() => { - expect(translateClient.translate.callCount).to.eq(0); - }); - }); -}); diff --git a/site/gatsby-site/cypress/e2e/unit/locale.cy.js b/site/gatsby-site/cypress/e2e/unit/locale.cy.js deleted file mode 100644 index 7507085a5a..0000000000 --- a/site/gatsby-site/cypress/e2e/unit/locale.cy.js +++ /dev/null @@ -1,43 +0,0 @@ -describe('Locale', () => { - it('Locale folder should contain specific JSON files for all specified languages', () => { - let availableLanguages = Cypress.env('availableLanguages'); - - let enFiles = []; - - cy.task('listFiles', `./i18n/locales/en`).then((files) => { - enFiles = files; - - if (availableLanguages) { - availableLanguages = availableLanguages.split(','); - - // check that each locale directory exists with the expected files - availableLanguages - .filter((a) => a !== 'en') - .forEach((locale) => { - cy.task('listFiles', `./i18n/locales/${locale}`).then((otherLocaleFiles) => { - expect(otherLocaleFiles.sort()).to.deep.equal( - enFiles.sort(), - `Locale ${locale} folder should contain the same files as the en folder` - ); - }); - }); - } - }); - }); - - it('should have a configuration for each available language', () => { - const configPath = './i18n/config.json'; - - cy.readFile(configPath).then((configurations) => { - expect(configurations).to.be.an('array'); - - const availableLanguages = Cypress.env('availableLanguages').split(','); - - availableLanguages.forEach((locale) => { - const hasConfig = configurations.some((config) => config.code === locale); - - expect(hasConfig, `Locale ${locale} should have configuration`).to.be.true; - }); - }); - }); -}); diff --git a/site/gatsby-site/cypress/e2e/unit/risksApi.cy.js b/site/gatsby-site/cypress/e2e/unit/risksApi.cy.js deleted file mode 100644 index 52c49a5ad9..0000000000 --- a/site/gatsby-site/cypress/e2e/unit/risksApi.cy.js +++ /dev/null @@ -1,59 +0,0 @@ -const { gql } = require('@apollo/client'); - -describe('Risks', () => { - it('Should retrieve a risk by query tag', () => { - cy.query({ - query: gql` - { - risks(input: { tags: ["GMF:Known AI Technology:Content-based Filtering"] }) { - tag - precedents { - title - incident_id - description - } - } - } - `, - timeout: 60000, // mongodb admin api is extremely slow - }).then(({ data: { risks } }) => { - const failureTag = 'GMF:Known AI Technical Failure:Adversarial Data'; - - const risk = risks.find((r) => r.tag == failureTag); - - const precedent = risk.precedents.find((p) => p.incident_id == 1); - - cy.expect(precedent).not.to.be.null; - cy.expect(precedent.title.length > 0).to.be.true; - cy.expect(precedent.description.length > 0).to.be.true; - }); - }); - - it('Should retrieve risks with no tag provided.', () => { - cy.query({ - query: gql` - { - risks { - tag - precedents { - title - incident_id - description - } - } - } - `, - timeout: 60000, // mongodb admin api is extremely slow - }).then(({ data: { risks } }) => { - const queryTag = 'GMF:Known AI Technical Failure:Adversarial Data'; - - const risk = risks.find((r) => r.tag == queryTag); - - const precedent = risk.precedents.find((p) => p.incident_id == 1); - - cy.expect(precedent).not.to.be.null; - cy.expect(precedent.title.length > 0).to.be.true; - cy.expect(precedent.description.length > 0).to.be.true; - }); - }); -}); diff --git a/site/gatsby-site/package-lock.json b/site/gatsby-site/package-lock.json index 9b80acf1ac..502d4a8291 100644 --- a/site/gatsby-site/package-lock.json +++ b/site/gatsby-site/package-lock.json @@ -108,6 +108,7 @@ "rollbar": "^2.26.3", "sass": "^1.54.5", "sharp": "^0.32.6", + "sinon": "^18.0.0", "slugify": "^1.6.5", "stemmer": "^1.0.5", "stopword": "^1.0.7", @@ -132,6 +133,7 @@ "@tailwindcss/typography": "^0.5.8", "@types/jest": "^29.5.12", "@types/node": "^20.12.13", + "@types/sinon": "^17.0.3", "@types/supertest": "^6.0.2", "autoprefixer": "^10.4.7", "babel-eslint": "^10.1.0", @@ -11833,7 +11835,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "dev": true, "dependencies": { "type-detect": "4.0.8" } @@ -11847,6 +11848,29 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==" + }, "node_modules/@smithy/abort-controller": { "version": "1.1.0", "license": "Apache-2.0", @@ -13280,6 +13304,15 @@ "integrity": "sha512-9Hp0ObzwwO57DpLFF0InUjUm/II8GmKAvzbefxQTihCb7KI6yc9yzf0nLc4mVdby5N4DRCgQM2wCup9KTieeww==", "dev": true }, + "node_modules/@types/sinon": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.3.tgz", + "integrity": "sha512-j3uovdn8ewky9kRBG19bOwaZbexJu/XjtkHyjvUgt4xfPFz18dcORIMqnYh66Fx3Powhcr85NT5+er3+oViapw==", + "dev": true, + "dependencies": { + "@types/sinonjs__fake-timers": "*" + } + }, "node_modules/@types/sinonjs__fake-timers": { "version": "8.1.1", "dev": true, @@ -18507,8 +18540,9 @@ "license": "Apache-2.0" }, "node_modules/diff": { - "version": "5.1.0", - "license": "BSD-3-Clause", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", "engines": { "node": ">=0.3.1" } @@ -28755,6 +28789,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==" + }, "node_modules/jwa": { "version": "2.0.0", "license": "MIT", @@ -32355,6 +32394,31 @@ "version": "1.0.5", "license": "MIT" }, + "node_modules/nise": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-6.0.0.tgz", + "integrity": "sha512-K8ePqo9BFvN31HXwEtTNGzgrPpmvgciDsFz8aztFjt4LqKO/JeFD8tBOeuDiCMXrIl/m1YvfH8auSpxfaD09wg==", + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/text-encoding": "^0.7.2", + "just-extend": "^6.2.0", + "path-to-regexp": "^6.2.1" + } + }, + "node_modules/nise/node_modules/@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/nise/node_modules/path-to-regexp": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz", + "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==" + }, "node_modules/no-case": { "version": "3.0.4", "license": "MIT", @@ -38173,6 +38237,50 @@ "version": "0.3.2", "license": "MIT" }, + "node_modules/sinon": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-18.0.0.tgz", + "integrity": "sha512-+dXDXzD1sBO6HlmZDd7mXZCR/y5ECiEiGCBSGuFD/kZ0bDTofPYc6JaeGmPSF+1j1MejGUWkORbYOLDyvqCWpA==", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.2.0", + "nise": "^6.0.0", + "supports-color": "^7" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/sinon/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/sinon/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "license": "MIT" @@ -40394,7 +40502,6 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, "engines": { "node": ">=4" } diff --git a/site/gatsby-site/package.json b/site/gatsby-site/package.json index d9aeda4722..9822ded4a9 100644 --- a/site/gatsby-site/package.json +++ b/site/gatsby-site/package.json @@ -104,6 +104,7 @@ "rollbar": "^2.26.3", "sass": "^1.54.5", "sharp": "^0.32.6", + "sinon": "^18.0.0", "slugify": "^1.6.5", "stemmer": "^1.0.5", "stopword": "^1.0.7", @@ -145,6 +146,7 @@ "@tailwindcss/typography": "^0.5.8", "@types/jest": "^29.5.12", "@types/node": "^20.12.13", + "@types/sinon": "^17.0.3", "@types/supertest": "^6.0.2", "autoprefixer": "^10.4.7", "babel-eslint": "^10.1.0", diff --git a/site/gatsby-site/playwright/config.ts b/site/gatsby-site/playwright/config.ts index dc40455df5..2440f27287 100644 --- a/site/gatsby-site/playwright/config.ts +++ b/site/gatsby-site/playwright/config.ts @@ -2,6 +2,7 @@ type ConfigType = { E2E_ADMIN_PASSWORD: string; E2E_ADMIN_USERNAME: string; IS_EMPTY_ENVIRONMENT: string; + AVAILABLE_LANGUAGES?: string; [key: string]: string; }; @@ -9,6 +10,7 @@ const config: ConfigType = { E2E_ADMIN_PASSWORD: process.env.E2E_ADMIN_PASSWORD!, E2E_ADMIN_USERNAME: process.env.E2E_ADMIN_USERNAME!, IS_EMPTY_ENVIRONMENT: process.env.IS_EMPTY_ENVIRONMENT ?? '', + AVAILABLE_LANGUAGES: process.env.GATSBY_AVAILABLE_LANGUAGES ?? '', } Object.keys(config).forEach((key) => { diff --git a/site/gatsby-site/playwright/e2e/citeEdit.spec.ts b/site/gatsby-site/playwright/e2e/citeEdit.spec.ts new file mode 100644 index 0000000000..364979ea9b --- /dev/null +++ b/site/gatsby-site/playwright/e2e/citeEdit.spec.ts @@ -0,0 +1,926 @@ +import { conditionalIntercept, waitForRequest, query, setEditorText, getEditorText, test } from '../utils'; +import { format, getUnixTime } from 'date-fns'; +import { gql } from '@apollo/client'; +import { expect } from '@playwright/test'; +import config from '../config'; +import reportWithTranslations from '../fixtures/reports/reportWithTranslations.json'; +import report10 from '../fixtures/reports/report.json'; +import updateOneReport from '../fixtures/reports/updateOneReport.json'; +import updateOneReportTranslation from '../fixtures/reports/updateOneReportTranslation.json'; +import issueWithTranslations from '../fixtures/reports/issueWithTranslations.json'; + +test.describe('Edit report', () => { + const url = '/cite/edit?report_number=10'; + + let user; + + test.beforeAll(async () => { + const response = await query({ + query: gql` + { + user(query: { first_name: "Test", last_name: "User" }) { + userId + first_name + last_name + } + } + `, + }); + user = response.data.user; + }); + + test('Successfully loads', async ({ page }) => { + await page.goto(url); + await page.evaluate(() => { + document.querySelectorAll('*').forEach(el => { + el.style.scrollBehavior = 'auto'; + }); + }); + }); + + test('Should load and update report values', async ({ page, login }) => { + await login(config.E2E_ADMIN_USERNAME, config.E2E_ADMIN_PASSWORD); + + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'FindReportWithTranslations', + reportWithTranslations, + 'FindReportWithTranslations' + ); + + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'FindReport', + report10, + 'FindReport' + ); + + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'FindIncidents', + { + data: { + incidents: [ + { + _typename: 'Incident', + incident_id: 1, + title: 'Incident 1', + }, + ], + }, + }, + 'FindIncidents' + ); + + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'FindIncidentsTitles', + { + data: { + incidents: [ + { + _typename: 'Incident', + incident_id: 1, + title: 'Incident 1', + }, + { + _typename: 'Incident', + incident_id: 2, + title: 'Incident 2', + }, + ], + }, + }, + 'FindIncidentsTitles' + ); + + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'logReportHistory', + { + data: { + logReportHistory: { + report_number: 10, + }, + }, + }, + 'logReportHistory' + ); + + await page.goto(url); + + await Promise.all([ + waitForRequest('FindReportWithTranslations'), + waitForRequest('FindIncidents'), + waitForRequest('FindReport') + ]); + + [ + 'authors', + 'date_downloaded', + 'date_published', + 'image_url', + 'submitters', + 'title', + 'editor_notes', + ].forEach(async (key) => { + const locator = page.locator(`[name=${key}]`); + await expect(locator).toHaveValue(reportWithTranslations.data.report[key].toString()); + }); + + let editorText = await getEditorText(page); + await expect(editorText).toBe(reportWithTranslations.data.report.text); + + await expect(page.locator('label:has-text("Incident IDs") + * [data-cy="token"]:has-text("Incident 1")')).toBeVisible(); + + + await expect(page.locator('.submit-report-tags [option="Test Tag"]')).toHaveCount(1); + + await expect(page.locator('[data-cy="translation-es"] [type="text"]')).toHaveValue(reportWithTranslations.data.report.translations_es.title); + + editorText = await getEditorText(page, '[data-cy="translation-es"] .CodeMirror'); + await expect(editorText).toBe(reportWithTranslations.data.report.translations_es.text); + + const updates = { + authors: 'Test Author', + date_downloaded: '2022-01-01', + date_published: '2022-02-02', + image_url: 'https://test.com/test.jpg', + submitters: 'Test Submitter', + title: 'Test Title', + url: 'https://www.test.com/test', + editor_notes: 'Pro iustitia tantum', + }; + + for (const [key, value] of Object.entries(updates)) { + const locator = page.locator(`[name=${key}]`); + await locator.fill(''); + await locator.fill(value); + } + + await page.locator(`[name="quiet"]`).click(); + + await setEditorText(page, '## This is text in English\n\nthat is longer that eighty characters, yes eighty characters!'); + + await page.locator('[id^=submit-report-tags]').fill('New Tag'); + await page.locator('a[aria-label="New Tag"]').click(); + + await page.locator('[data-cy="translation-es"] [type="text"]').fill('Este es un titulo en Espanol!'); + + await setEditorText(page, '## Este es texto en espanol\n\nque es mas largo que ochenta caracters, si ochenta caracteres!', '[data-cy="translation-es"] .CodeMirror'); + + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'UpdateReport', + updateOneReport, + 'updateReport' + ); + + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'UpdateReportTranslation', + updateOneReportTranslation, + 'updateOneReportTranslation' + ); + + const now = new Date(); + await page.context().addInitScript(`{ + const now = ${now.getTime()}; + Date.now = () => now; + const originalDate = Date; + globalThis.Date = class extends originalDate { + constructor(...args) { + if (args.length === 0) { + super(now); + } else { + super(...args); + } + } + } + }`); + + await page.getByRole('button', { name: 'Submit' }).click(); + + const updateReportRequest = await waitForRequest('updateReport'); + const variables = updateReportRequest.postDataJSON().variables; + + const expectedReport = { + authors: ['Test Author'], + cloudinary_id: 'reports/test.com/test.jpg', + date_downloaded: new Date('2022-01-01').toISOString(), + date_modified: format(now, 'yyyy-MM-dd'), + date_published: new Date('2022-02-02').toISOString(), + epoch_date_modified: getUnixTime(now), + epoch_date_published: 1643760000, + flag: null, + image_url: 'https://test.com/test.jpg', + report_number: 10, + submitters: ['Test Submitter'], + tags: ['Test Tag', 'New Tag'], + text: '## This is text in English\n\nthat is longer that eighty characters, yes eighty characters!', + plain_text: + 'This is text in English\n\nthat is longer that eighty characters, yes eighty characters!\n', + title: 'Test Title', + url: 'https://www.test.com/test', + source_domain: 'test.com', + editor_notes: 'Pro iustitia tantum', + language: 'en', + quiet: true, + }; + + expect(variables.query.report_number).toBe(10); + + const expectedResult = { + ...expectedReport, + date_modified: format(new Date(variables.set.date_modified), 'yyyy-MM-dd') + } + + expect({ + ...variables.set, + date_modified: format(new Date(variables.set.date_modified), 'yyyy-MM-dd') + } + ).toEqual(expectedResult); + + const logReportHistoryRequest = await waitForRequest('logReportHistory'); + const input = logReportHistoryRequest.postDataJSON().variables.input; + const expectedReportResult = { + ...report10.data.report, + ...expectedReport, + modifiedBy: user.userId, + user: report10.data.report.user.userId, + date_modified: input.date_modified, + }; + + expect(input).toEqual(expectedReportResult); + + const updateOneReportTranslationRequest = await waitForRequest('updateOneReportTranslation'); + const translationVariables = updateOneReportTranslationRequest.postDataJSON().variables.input; + expect(translationVariables.language).toBe('es'); + expect(translationVariables.report_number).toBe(10); + expect(translationVariables.text).toBe('## Este es texto en espanol\n\nque es mas largo que ochenta caracters, si ochenta caracteres!'); + expect(translationVariables.plain_text).toBe('Este es texto en espanol\n\nque es mas largo que ochenta caracters, si ochenta caracteres!\n'); + expect(translationVariables.title).toBe('Este es un titulo en Espanol!'); + + await expect(page.getByText('Incident report 10 updated successfully.')).toBeVisible(); + }); + + test('Should load and update Issue values', async ({ page, login }) => { + await login(config.E2E_ADMIN_USERNAME, config.E2E_ADMIN_PASSWORD); + + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'FindReportWithTranslations', + issueWithTranslations, + 'FindReportWithTranslations' + ); + + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'FindReport', + report10, + 'FindReport' + ); + + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'FindIncidents', + { data: { incidents: [] } }, + 'FindIncidents' + ); + + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'FindIncidentsTitles', + { data: { incidents: [{ _typename: 'Incident', incident_id: 1, title: 'Incident 1' }] } }, + 'FindIncidentsTitles' + ); + + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'logReportHistory', + { data: { logReportHistory: { report_number: 10 } } }, + 'logReportHistory' + ); + + await page.goto(url); + + await Promise.all([ + waitForRequest('FindReportWithTranslations'), + waitForRequest('FindIncidents'), + waitForRequest('FindReport') + ]); + + for (const key of ['authors', 'date_downloaded', 'date_published', 'image_url', 'submitters', 'title', 'editor_notes']) { + await expect(page.locator(`[name=${key}]`)).toHaveValue(reportWithTranslations.data.report[key].toString()); + } + + const text = await getEditorText(page); + expect(text).toBe(reportWithTranslations.data.report.text); + + await expect(page.locator(`[name="incident_id"]`)).not.toBeVisible(); + + await expect(page.locator('.submit-report-tags [option="Test Tag"]')).toHaveCount(1); + + await expect(page.locator('[data-cy="translation-es"] [type="text"]')).toHaveValue(reportWithTranslations.data.report.translations_es.title); + + const esText = await getEditorText(page, '[data-cy="translation-es"] .CodeMirror'); + expect(esText).toBe(reportWithTranslations.data.report.translations_es.text); + + const updates = { + authors: 'Test Author', + date_downloaded: '2022-01-01', + date_published: '2022-02-02', + image_url: 'https://test.com/test.jpg', + submitters: 'Test Submitter', + title: 'Test Title', + url: 'https://www.test.com/test', + editor_notes: 'Pro iustitia tantum' + }; + + for (const [key, value] of Object.entries(updates)) { + await page.locator(`[name=${key}]`).fill(value); + } + + await setEditorText(page, '## This is text in English\n\nthat is longer that eighty characters, yes eighty characters!', '[data-cy="text"] .CodeMirror'); + + await page.locator('[id^=submit-report-tags]').fill('New Tag'); + await page.locator('a[aria-label="New Tag"]').click(); + + await page.locator('[data-cy="translation-es"] [type="text"]').fill('Este es un titulo en Espanol!'); + + await setEditorText(page, '## Este es texto en espanol\n\nque es mas largo que ochenta caracters, si ochenta caracteres!', '[data-cy="translation-es"] .CodeMirror'); + + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'UpdateReport', + updateOneReport, + 'updateReport' + ); + + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'UpdateReportTranslation', + updateOneReportTranslation, + 'updateOneReportTranslation' + ); + + const now = new Date(); + await page.context().addInitScript(`Date = class extends Date { constructor() { super("${now.toISOString()}"); } }`); + + await page.getByRole('button', { name: 'Submit' }).click(); + + const expectedReport = { + authors: ['Test Author'], + cloudinary_id: 'reports/test.com/test.jpg', + date_downloaded: new Date('2022-01-01').toISOString(), + date_modified: format(now, 'yyyy-MM-dd'), + date_published: new Date('2022-02-02').toISOString(), + epoch_date_modified: getUnixTime(now), + epoch_date_published: 1643760000, + flag: null, + image_url: 'https://test.com/test.jpg', + report_number: 10, + submitters: ['Test Submitter'], + tags: ['Test Tag', 'New Tag'], + text: '## This is text in English\n\nthat is longer that eighty characters, yes eighty characters!', + plain_text: 'This is text in English\n\nthat is longer that eighty characters, yes eighty characters!\n', + title: 'Test Title', + url: 'https://www.test.com/test', + source_domain: 'test.com', + editor_notes: 'Pro iustitia tantum', + language: 'en' + }; + + const updateReportRequest = await waitForRequest('updateReport'); + const variables = updateReportRequest.postDataJSON().variables; + expect(variables.query.report_number).toBe(10); + expect({ ...variables.set, date_modified: format(new Date(variables.set.date_modified), 'yyyy-MM-dd') }).toEqual(expectedReport); + + const logReportHistoryRequest = await waitForRequest('logReportHistory'); + const input = logReportHistoryRequest.postDataJSON().variables.input; + const expectedResult = { ...report10.data.report, ...expectedReport, modifiedBy: user.userId, user: report10.data.report.user.userId, date_modified: input.date_modified }; + expect(input).toEqual(expectedResult); + + const updateOneReportTranslationRequest = await waitForRequest('updateOneReportTranslation'); + const translationVariables = updateOneReportTranslationRequest.postDataJSON().variables.input; + expect(translationVariables.language).toBe('es'); + expect(translationVariables.report_number).toBe(10); + expect(translationVariables.text).toBe('## Este es texto en espanol\n\nque es mas largo que ochenta caracters, si ochenta caracteres!'); + expect(translationVariables.plain_text).toBe('Este es texto en espanol\n\nque es mas largo que ochenta caracters, si ochenta caracteres!\n'); + expect(translationVariables.title).toBe('Este es un titulo en Espanol!'); + + await expect(page.getByText('Issue 10 updated successfully')).toBeVisible(); + }); + + test('Should delete incident report', async ({ page, login }) => { + await login(config.E2E_ADMIN_USERNAME, config.E2E_ADMIN_PASSWORD); + + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'FindReportWithTranslations', + issueWithTranslations, + 'FindReportWithTranslations' + ); + + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'FindIncidents', + { data: { incidents: [] } }, + 'FindIncidents' + ); + + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'FindIncidentsTitles', + { + data: { + incidents: [ + { _typename: 'Incident', incident_id: 1, title: 'Incident 1' } + ] + } + }, + 'FindIncidentsTitles' + ); + + await page.goto(url); + + await Promise.all([ + waitForRequest('FindIncidents'), + waitForRequest('FindIncidentsTitles'), + waitForRequest('FindReportWithTranslations') + ]); + + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'DeleteOneReport', + { data: { deleteOneReport: { __typename: 'Report', report_number: 10 } } }, + 'DeleteReport' + ); + + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'LinkReportsToIncidents', + { data: { linkReportsToIncidents: [] } }, + 'LinkReportsToIncidents' + ); + + // Set up the dialog event listener before triggering the click action + page.once('dialog', async dialog => { + await dialog.accept(); + + const deleteRequest = await waitForRequest('DeleteReport'); + expect(deleteRequest.postDataJSON().variables.query).toEqual({ report_number: 10 }); + + const linkReportsToIncidentsRequest = await waitForRequest('LinkReportsToIncidents'); + expect(linkReportsToIncidentsRequest.postDataJSON().variables.input).toEqual({ + incident_ids: [], + report_numbers: [10] + }); + + await expect(page.getByText('Incident report 10 deleted successfully')).toBeVisible(); + + }); + + await page.getByText('Delete this report').click(); + + await page.waitForTimeout(2000); // Needed to wait for the dialog to be accepted + }); + + test('Should link a report to another incident', async ({ page, login }) => { + await login(config.E2E_ADMIN_USERNAME, config.E2E_ADMIN_PASSWORD); + + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'FindReport', + report10, + 'FindReport' + ); + + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'ProbablyRelatedReports', + { data: { reports: [] } }, + 'ProbablyRelatedReports' + ); + + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'ProbablyRelatedIncidents', + { data: { incidents: [] } }, + 'ProbablyRelatedIncidents' + ); + + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'FindReportWithTranslations', + reportWithTranslations, + 'FindReportWithTranslations' + ); + + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'FindIncidents', + { + data: { + incidents: [ + { + __typename: 'Incident', + incident_id: 1, + title: 'Incident 1', + }, + ], + }, + }, + 'FindIncidents' + ); + + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'FindIncidentsTitles', + { + data: { + incidents: [ + { + _typename: 'Incident', + incident_id: 1, + title: 'Incident 1', + }, + { + _typename: 'Incident', + incident_id: 2, + title: 'Incident 2', + }, + ], + }, + }, + 'FindIncidentsTitles' + ); + + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'logReportHistory', + { + data: { + logReportHistory: { + report_number: 10, + }, + }, + }, + 'logReportHistory' + ); + + await page.goto(`/cite/edit?report_number=23`); + + await Promise.all([ + waitForRequest('FindReportWithTranslations'), + waitForRequest('FindIncidents'), + waitForRequest('FindIncidentsTitles') + ]); + + await expect(page.locator('form[data-cy="report"]')).toBeVisible(); + + const incidentDiv = page.locator('div:has-text("Incident 1")'); + + await incidentDiv.locator('xpath=following-sibling::button[1]').click(); + + await page.locator('[name="incident_ids"]').fill('2'); + + await page.locator('[id="incident_ids-item-0"]').click(); + + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'UpdateReportTranslation', + updateOneReportTranslation, + 'updateOneReportTranslationSpanish' + ); + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'UpdateReportTranslation' && req.postDataJSON().variables.input.language === 'fr', + updateOneReportTranslation, + 'updateOneReportTranslationFrench' + ); + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'UpdateReportTranslation' && req.postDataJSON().variables.input.language === 'ja', + updateOneReportTranslation, + 'updateOneReportTranslationJapanese' + ); + + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'UpdateReport', + updateOneReport, + 'UpdateReport' + ); + + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'LinkReportsToIncidents', + { data: { linkReportsToIncidents: [] } }, + 'LinkReportsToIncidents' + ); + + await page.getByRole('button', { name: 'Submit' }).click(); + + let now = new Date(); + await page.addInitScript(`{ + Date.now = () => ${now.getTime()}; + }`); + + const expectedReport = { + authors: ['Marco Acevedo'], + cloudinary_id: 'reports/assets.change.org/photos/0/yb/id/eYyBIdJOMHpqcty-1600x900-noPad.jpg?1523726975', + date_downloaded: new Date('2019-04-13').toISOString(), + date_modified: format(now, 'yyyy-MM-dd'), + date_published: new Date('2015-07-11').toISOString(), + editor_notes: '', + epoch_date_published: 1436572800, + flag: null, + image_url: 'https://assets.change.org/photos/0/yb/id/eYyBIdJOMHpqcty-1600x900-noPad.jpg?1523726975', + language: 'en', + plain_text: 'Video still of a reproduced version of Minnie Mouse\n\nWhich appeared on the now-suspended Simple Fun channel Simple Fun.\n', + report_number: 10, + source_domain: 'change.org', + submitters: ['Roman Yampolskiy'], + tags: ['Test Tag'], + text: '## Video still of a reproduced version of Minnie Mouse\n\nWhich appeared on the now-suspended Simple Fun channel Simple Fun.', + title: 'Remove YouTube Kids app until it eliminates its inappropriate content', + url: 'https://www.change.org/p/remove-youtube-kids-app-until-it-eliminates-its-inappropriate-content', + epoch_date_modified: null, + }; + + now = new Date(); + const updateReportRequest = await waitForRequest('UpdateReport'); + const variables = updateReportRequest.postDataJSON().variables; + expect(variables.query.report_number).toBe(23); + + expect({ + ...variables.set, + date_modified: format(new Date(variables.set.date_modified), 'yyyy-MM-dd'), + }).toEqual({ + ...expectedReport, + epoch_date_modified: getUnixTime(now), + }); + + const expectedNow = new Date(); + const logReportHistoryRequest = await waitForRequest('logReportHistory'); + const input = logReportHistoryRequest.postDataJSON().variables.input; + const expectedResult = { + ...report10.data.report, + ...expectedReport, + modifiedBy: user.userId, + user: report10.data.report.user.userId, + date_modified: input.date_modified, + epoch_date_modified: getUnixTime(now), + }; + expect(input).toEqual(expectedResult); + + const updateOneReportTranslationRequest1 = await waitForRequest('updateOneReportTranslationSpanish'); + let translationVariables = updateOneReportTranslationRequest1.postDataJSON().variables; + expect(translationVariables.input.title).toBe('Este es el Título en español'); + expect(translationVariables.input.text).toBe('Este es un texto de prueba que tiene un largo mayor a ochenta caracteres (en español)'); + expect(translationVariables.input.language).toBe('es'); + expect(translationVariables.input.report_number).toBe(23); + expect(translationVariables.input.plain_text).toBe('Este es un texto de prueba que tiene un largo mayor a ochenta caracteres (en español)\n'); + + const updateOneReportTranslationRequest2 = await waitForRequest('updateOneReportTranslationFrench'); + translationVariables = updateOneReportTranslationRequest2.postDataJSON().variables; + expect(translationVariables.input.title).toBe(`C'est le Titre en français`); + expect(translationVariables.input.text).toBe(`Il s'agit d'un texte de test de plus de quatre-vingts caractères - lorem ipsum (en français)`); + expect(translationVariables.input.language).toBe('fr'); + expect(translationVariables.input.report_number).toBe(23); + expect(translationVariables.input.plain_text).toBe(`Il s'agit d'un texte de test de plus de quatre-vingts caractères - lorem ipsum (en français)\n`); + + const updateOneReportTranslationRequest3 = await waitForRequest('updateOneReportTranslationJapanese'); + translationVariables = updateOneReportTranslationRequest3.postDataJSON().variables; + expect(translationVariables.input.title).toBe('これは日本語でのタイトルです'); + expect(translationVariables.input.text).toBe('解サオライ協立なーづ民手ぶみドに即記朝ぐ奥置ぱで地更トるあて栄厚ぜづを祭屋ん来派どてゃ読速ヘ誌約カタシネ原39業理る。外ヒヱフ社第むせゆ由更混ソエ夕野しりすよ顔飛リの兆基う公言や置17謝后嘘5供フキヌア星集ヘラ辞勘壇崇さびわ。(日本語で)'); + expect(translationVariables.input.language).toBe('ja'); + expect(translationVariables.input.report_number).toBe(23); + expect(translationVariables.input.plain_text).toBe('解サオライ協立なーづ民手ぶみドに即記朝ぐ奥置ぱで地更トるあて栄厚ぜづを祭屋ん来派どてゃ読速ヘ誌約カタシネ原39業理る。外ヒヱフ社第むせゆ由更混ソエ夕野しりすよ顔飛リの兆基う公言や置17謝后嘘5供フキヌア星集ヘラ辞勘壇崇さびわ。(日本語で)\n'); + + const linkReportsToIncidentsRequest = await waitForRequest('LinkReportsToIncidents'); + expect(linkReportsToIncidentsRequest.postDataJSON().variables.input).toEqual({ + incident_ids: [2], + report_numbers: [23], + }); + + await expect(page.locator('[data-cy="toast"]')).toContainText('Incident report 23 updated successfully'); + }); + + test('Should convert an incident report to an issue', async ({ page, login }) => { + await login(config.E2E_ADMIN_USERNAME, config.E2E_ADMIN_PASSWORD); + + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'FindReport', + report10, + 'FindReport' + ); + + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'ProbablyRelatedReports', + { data: { reports: [] } }, + 'ProbablyRelatedReports' + ); + + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'ProbablyRelatedIncidents', + { data: { incidents: [] } }, + 'ProbablyRelatedIncidents' + ); + + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'FindReportWithTranslations', + reportWithTranslations, + 'FindReportWithTranslations' + ); + + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'FindIncidents', + { + data: { + incidents: [ + { + _typename: 'Incident', + incident_id: 1, + title: 'Incident 1', + }, + ], + }, + }, + 'FindIncidents' + ); + + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'FindIncidentsTitles', + { + data: { + incidents: [ + { _typename: 'Incident', incident_id: 1, title: 'Incident 1' }, + { _typename: 'Incident', incident_id: 2, title: 'Incident 2' }, + ], + }, + }, + 'FindIncidentsTitles' + ); + + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'logReportHistory', + { data: { logReportHistory: { report_number: 10 } } }, + 'logReportHistory' + ); + + await page.goto(`/cite/edit?report_number=23`); + + await page.locator('form[data-cy="report"]').waitFor(); + + const incidentDiv = page.locator('div:has-text("Incident 1")'); + + await incidentDiv.locator('xpath=following-sibling::button[1]').click(); + + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'UpdateReport', + updateOneReport, + 'UpdateReport' + ); + + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'UpdateReportTranslation', + updateOneReportTranslation, + 'UpdateReportTranslation' + ); + + await conditionalIntercept( + page, + '**/graphql', + (req) => req.postDataJSON().operationName == 'LinkReportsToIncidents', + { data: { linkReportsToIncidents: [] } }, + 'LinkReportsToIncidents' + ); + + await page.evaluate(() => window.confirm = () => true); + + const now = new Date(); + + await page.addInitScript(`{ + Date.now = () => ${now.getTime()}; + }`); + + await page.getByRole('button', { name: 'Submit' }).click(); + + const expectedReport = { + authors: ['Marco Acevedo'], + cloudinary_id: + 'reports/assets.change.org/photos/0/yb/id/eYyBIdJOMHpqcty-1600x900-noPad.jpg?1523726975', + date_downloaded: new Date('2019-04-13').toISOString(), + date_published: new Date('2015-07-11').toISOString(), + flag: null, + image_url: + 'https://assets.change.org/photos/0/yb/id/eYyBIdJOMHpqcty-1600x900-noPad.jpg?1523726975', + report_number: 10, + submitters: ['Roman Yampolskiy'], + tags: ['Test Tag'], + text: '## Video still of a reproduced version of Minnie Mouse\n\nWhich appeared on the now-suspended Simple Fun channel Simple Fun.', + plain_text: + 'Video still of a reproduced version of Minnie Mouse\n\nWhich appeared on the now-suspended Simple Fun channel Simple Fun.\n', + title: 'Remove YouTube Kids app until it eliminates its inappropriate content', + url: 'https://www.change.org/p/remove-youtube-kids-app-until-it-eliminates-its-inappropriate-content', + editor_notes: '', + language: 'en', + source_domain: 'change.org', + epoch_date_published: 1436572800, + date_modified: format(now, 'yyyy-MM-dd'), + epoch_date_modified: getUnixTime(now), + }; + + const updateReportRequest = await waitForRequest('UpdateReport'); + const variables = updateReportRequest.postDataJSON().variables; + expect(variables.query.report_number).toBe(23); + expect({ + ...variables.set, + date_modified: format(new Date(variables.set.date_modified), 'yyyy-MM-dd'), + }).toEqual(expectedReport); + + const logReportHistoryRequest = await waitForRequest('logReportHistory'); + const input = logReportHistoryRequest.postDataJSON().variables.input; + const expectedResult = { + ...report10.data.report, + ...expectedReport, + modifiedBy: user.userId, + user: report10.data.report.user.userId, + date_modified: input.date_modified, + }; + + expect(input).toEqual(expectedResult); + + await waitForRequest('UpdateReportTranslation'); + await waitForRequest('UpdateReportTranslation'); + + const linkReportsToIncidentsRequest = await waitForRequest('LinkReportsToIncidents'); + expect(linkReportsToIncidentsRequest.postDataJSON().variables.input).toEqual({ + incident_ids: [], + report_numbers: [23], + }); + + await page.getByText('Issue 23 updated successfully').waitFor(); + }); + + test('Should display the report image', async ({ page, login }) => { + await login(config.E2E_ADMIN_USERNAME, config.E2E_ADMIN_PASSWORD); + + await page.goto(url); + + await page.locator('[data-cy="image-preview-figure"] img').waitFor(); + const imgSrc = await page.locator('[data-cy="image-preview-figure"] img').getAttribute('src'); + expect(imgSrc).toBe('https://res.cloudinary.com/pai/image/upload/f_auto/q_auto/v1/reports/assets.change.org/photos/0/yb/id/eYyBIdJOMHpqcty-1600x900-noPad.jpg?1523726975'); + }); +}); \ No newline at end of file diff --git a/site/gatsby-site/playwright/e2e/unit/locale.spec.ts b/site/gatsby-site/playwright/e2e/unit/locale.spec.ts new file mode 100644 index 0000000000..b925223f50 --- /dev/null +++ b/site/gatsby-site/playwright/e2e/unit/locale.spec.ts @@ -0,0 +1,38 @@ +import { expect } from '@playwright/test'; +import { test, listFiles } from '../../utils'; +import { readFileSync } from 'fs'; +import config from '../../config'; + +test.describe('Locale', () => { + test('Locale folder should contain specific JSON files for all specified languages', async ({ }) => { + let availableLanguages: string = config.AVAILABLE_LANGUAGES; + let enFiles: string[] = []; + + enFiles = await listFiles('./i18n/locales/en'); + + if (availableLanguages) { + const splittedLanguages: string[] = (availableLanguages as string).split(','); + + for (const locale of splittedLanguages.filter((a) => a !== 'en')) { + const otherLocaleFiles = await listFiles(`./i18n/locales/${locale}`); + expect(otherLocaleFiles.sort()).toEqual( + enFiles.sort() + ); + } + } + }); + + test('should have a configuration for each available language', async ({ }) => { + const configPath = './i18n/config.json'; + const configurations = JSON.parse(readFileSync(configPath, 'utf-8')); + + expect(configurations).toBeInstanceOf(Array); + + const availableLanguages = config.AVAILABLE_LANGUAGES.split(','); + + availableLanguages.forEach((locale) => { + const hasConfig = configurations.some((config) => config.code === locale); + expect(hasConfig).toBe(true); + }); + }); +}); diff --git a/site/gatsby-site/playwright/e2e/unit/risksApi.spec.ts b/site/gatsby-site/playwright/e2e/unit/risksApi.spec.ts new file mode 100644 index 0000000000..39a5c8cd26 --- /dev/null +++ b/site/gatsby-site/playwright/e2e/unit/risksApi.spec.ts @@ -0,0 +1,56 @@ + +import { gql } from '@apollo/client'; +import { query, test } from '../../utils'; +import { expect } from '@playwright/test'; + +test('Should retrieve a risk by query tag', async () => { + const result = await query({ + query: gql` + { + risks(input: { tags: ["GMF:Known AI Technology:Content-based Filtering"] }) { + tag + precedents { + title + incident_id + description + } + } + } + ` + }); + + const { risks } = result.data; + const failureTag = 'GMF:Known AI Technical Failure:Adversarial Data'; + const risk = risks.find((r) => r.tag === failureTag); + const precedent = risk.precedents.find((p) => p.incident_id === 1); + + expect(precedent).not.toBeNull(); + expect(precedent.title.length > 0).toBe(true); + expect(precedent.description.length > 0).toBe(true); +}); + +test('Should retrieve risks with no tag provided.', async () => { + const result = await query({ + query: gql` + { + risks { + tag + precedents { + title + incident_id + description + } + } + } + ` + }); + + const { risks } = result.data; + const queryTag = 'GMF:Known AI Technical Failure:Adversarial Data'; + const risk = risks.find((r) => r.tag === queryTag); + const precedent = risk.precedents.find((p) => p.incident_id === 1); + + expect(precedent).not.toBeNull(); + expect(precedent.title.length > 0).toBe(true); + expect(precedent.description.length > 0).toBe(true); +}); diff --git a/site/gatsby-site/playwright/e2e/unit/translator.spec.ts b/site/gatsby-site/playwright/e2e/unit/translator.spec.ts new file mode 100644 index 0000000000..e998bbeb03 --- /dev/null +++ b/site/gatsby-site/playwright/e2e/unit/translator.spec.ts @@ -0,0 +1,210 @@ +import { ObjectId } from 'bson'; +import { test } from '../../utils'; +import Translator from '../../../src/utils/Translator'; +import sinon from 'sinon'; + +const reports = [ + { + _id: new ObjectId('60dd465f80935bc89e6f9b01'), + authors: ['Alistair Barr'], + date_downloaded: '2019-04-13', + date_modified: '2020-06-14', + date_published: '2015-05-19', + date_submitted: '2019-06-01', + description: 'Description of report 1', + epoch_date_downloaded: 1555113600, + epoch_date_modified: 1592092800, + epoch_date_published: 1431993600, + epoch_date_submitted: 1559347200, + image_url: 'http://url.com', + language: 'en', + report_number: 1, + source_domain: 'blogs.wsj.com', + submitters: ['Roman Yampolskiy'], + tags: [], + text: 'Report 1 **text**', + plain_text: 'Report 1 text', + title: 'Report 1 title', + url: 'https://url.com/stuff', + }, + { + _id: new ObjectId('60dd465f80935bc89e6f9b02'), + authors: ['Alistair Barr'], + date_downloaded: '2019-04-13', + date_modified: '2020-06-14', + date_published: '2015-05-19', + date_submitted: '2019-06-01', + description: 'Description of report 2', + epoch_date_downloaded: 1555113600, + epoch_date_modified: 1592092800, + epoch_date_published: 1431993600, + epoch_date_submitted: 1559347200, + image_url: 'http://url.com', + language: 'es', + report_number: 2, + source_domain: 'blogs.wsj.com', + submitters: ['Roman Yampolskiy'], + tags: [], + text: 'Report 2 **text**', + plain_text: 'Report 2 text', + title: 'Report 2 title', + url: 'https://url.com/stuff', + }, +]; + +test('Translations - Should translate languages only if report language differs from target language', async ({ page }) => { + const translatedReportsEN = [ + { + _id: '61d5ad9f102e6e30fca90ddf', + text: 'translated-en-text report 1', + title: 'translated-en-title report 1', + report_number: 1, + }, + ]; + + const translatedReportsES = [ + { + _id: '61d5ad9f102e6e30fca90ddf', + text: 'translated-es-text report 2', + title: 'translated-es-title report 2', + report_number: 2, + }, + ]; + + const reporter = { log: sinon.stub() }; + + const reportsCollection = { + find: sinon.stub().returns({ + toArray: sinon.stub().resolves(reports), + }), + }; + + const reportsENCollection = { + find: sinon.stub().returns({ + toArray: sinon.stub().resolves(translatedReportsEN), + }), + insertMany: sinon.stub().resolves({ insertedCount: 1 }), + }; + + const reportsESCollection = { + find: sinon.stub().returns({ + toArray: sinon.stub().resolves(translatedReportsES), + }), + insertMany: sinon.stub().resolves({ insertedCount: 1 }), + }; + + const mongoClient = { + connect: sinon.stub().resolves(), + close: sinon.stub().resolves(), + db: sinon.stub().returns({ + collection: (name: string) => { + if (name === 'reports') return reportsCollection; + if (name === 'reports_en') return reportsENCollection; + if (name === 'reports_es') return reportsESCollection; + return null; + }, + }), + }; + + const translateClient = { + translate: sinon.stub().callsFake((payload, { to }) => [payload.map((p: any) => `test-${to}-${p}`)]), + }; + + const translator = new Translator({ + mongoClient, + translateClient, + languages: [{ code: 'es' }, { code: 'en' }], + reporter, + dryRun: false, + }); + + await translator.run(); + + sinon.assert.calledOnce(mongoClient.connect); + sinon.assert.calledOnce(reportsENCollection.insertMany); + sinon.assert.calledWith(reportsENCollection.insertMany, [{ + report_number: 2, + text: 'test-en-Report 2 **text**', + title: 'test-en-Report 2 title', + plain_text: 'test-en-Report 2 text\n', + }]); + sinon.assert.calledOnce(reportsESCollection.insertMany); + sinon.assert.calledWith(reportsESCollection.insertMany, [{ + report_number: 1, + text: 'test-es-Report 1 **text**', + title: 'test-es-Report 1 title', + plain_text: 'test-es-Report 1 text\n', + }]); + sinon.assert.calledOnce(mongoClient.close); +}); + +test("Translations - Shouldn't call Google's translate api if dryRun is true", async ({ page }) => { + const translatedReportsEN = [ + { + _id: '61d5ad9f102e6e30fca90ddf', + text: 'translated-en-text report 1', + title: 'translated-en-title report 1', + report_number: 1, + }, + ]; + + const translatedReportsES = [ + { + _id: '61d5ad9f102e6e30fca90ddf', + text: 'translated-es-text report 2', + title: 'translated-es-title report 2', + report_number: 2, + }, + ]; + + const reporter = { log: sinon.stub() }; + + const reportsCollection = { + find: sinon.stub().returns({ + toArray: sinon.stub().resolves(reports), + }), + }; + + const reportsENCollection = { + find: sinon.stub().returns({ + toArray: sinon.stub().resolves(translatedReportsEN), + }), + insertMany: sinon.stub().resolves({ insertedCount: 1 }), + }; + + const reportsESCollection = { + find: sinon.stub().returns({ + toArray: sinon.stub().resolves(translatedReportsES), + }), + insertMany: sinon.stub().resolves({ insertedCount: 1 }), + }; + + const mongoClient = { + connect: sinon.stub().resolves(), + close: sinon.stub().resolves(), + db: sinon.stub().returns({ + collection: (name: string) => { + if (name === 'reports') return reportsCollection; + if (name === 'reports_en') return reportsENCollection; + if (name === 'reports_es') return reportsESCollection; + return null; + }, + }), + }; + + const translateClient = { + translate: sinon.stub().callsFake((payload, { to }) => [payload.map((p: any) => `translated-${to}-${p}`)]), + }; + + const translator = new Translator({ + mongoClient, + translateClient, + languages: [{ code: 'es' }, { code: 'en' }], + reporter, + dryRun: true, + }); + + await translator.run(); + + sinon.assert.notCalled(translateClient.translate); +}); diff --git a/site/gatsby-site/playwright/fixtures/reports/issueWithTranslations.json b/site/gatsby-site/playwright/fixtures/reports/issueWithTranslations.json new file mode 100644 index 0000000000..a304a6f318 --- /dev/null +++ b/site/gatsby-site/playwright/fixtures/reports/issueWithTranslations.json @@ -0,0 +1,50 @@ +{ + "data": { + "report": { + "authors": [ + "Marco Acevedo" + ], + "date_downloaded": "2019-04-13", + "date_published": "2015-07-11", + "flag": null, + "image_url": "https://assets.change.org/photos/0/yb/id/eYyBIdJOMHpqcty-1600x900-noPad.jpg?1523726975", + "report_number": 10, + "submitters": [ + "Roman Yampolskiy" + ], + "tags": [ + "Test Tag" + ], + "text": "## Video still of a reproduced version of Minnie Mouse\n\nWhich appeared on the now-suspended Simple Fun channel Simple Fun.", + "plain_text": "Video still of a reproduced version of Minnie Mouse\n\nWhich appeared on the now-suspended Simple Fun channel Simple Fun.", + "title": "Remove YouTube Kids app until it eliminates its inappropriate content", + "description": "Description Remove YouTube Kids app until it eliminates its inappropriate content", + "url": "https://www.change.org/p/remove-youtube-kids-app-until-it-eliminates-its-inappropriate-content", + "editor_notes": "", + "language": "en", + "translations_es": { + "__typename": "ReportTranslation", + "title": "Este es el Título en español", + "text": "Este es un texto de prueba que tiene un largo mayor a ochenta caracteres (en español)" + }, + "translations_en": { + "__typename": "ReportTranslation", + "title": "", + "text": "" + }, + "translations_fr": { + "__typename": "ReportTranslation", + "title": "C'est le Titre en français", + "text": "Il s'agit d'un texte de test de plus de quatre-vingts caractères - lorem ipsum (en français)" + }, + "translations_ja": { + "__typename": "ReportTranslation", + "title": "これは日本語でのタイトルです", + "text": "解サオライ協立なーづ民手ぶみドに即記朝ぐ奥置ぱで地更トるあて栄厚ぜづを祭屋ん来派どてゃ読速ヘ誌約カタシネ原39業理る。外ヒヱフ社第むせゆ由更混ソエ夕野しりすよ顔飛リの兆基う公言や置17謝后嘘5供フキヌア星集ヘラ辞勘壇崇さびわ。(日本語で)" + }, + "date_modified": "2023-01-01", + "epoch_date_modified": 1672531200, + "is_incident_report": false + } + } +} \ No newline at end of file diff --git a/site/gatsby-site/playwright/fixtures/reports/report.json b/site/gatsby-site/playwright/fixtures/reports/report.json new file mode 100644 index 0000000000..1ae223f554 --- /dev/null +++ b/site/gatsby-site/playwright/fixtures/reports/report.json @@ -0,0 +1,37 @@ +{ + "data": { + "report": { + "authors": ["Marco Acevedo"], + "cloudinary_id": "reports/assets.change.org/photos/0/yb/id/eYyBIdJOMHpqcty-1600x900-noPad.jpg?1523726975", + "date_downloaded": "2019-04-13", + "date_modified": "2020-06-14", + "date_published": "2015-07-11", + "date_submitted": "2019-06-01", + "editor_notes": "", + "embedding": { + "from_text_hash": "12256b4f3816b968a2224017eac1bf7d5c6b98e5", + "vector": [-0.09368586540222168, 0.02987837791442871, 0.03945714980363846] + }, + "epoch_date_downloaded": 1555113600, + "epoch_date_modified": 1592092800, + "epoch_date_published": 1436572800, + "epoch_date_submitted": 1559347200, + "flag": null, + "image_url": "https://assets.change.org/photos/0/yb/id/eYyBIdJOMHpqcty-1600x900-noPad.jpg?1523726975", + "is_incident_report": true, + "language": "en", + "plain_text": "Videos filled with profanity, sexually explicit material, alcohol, smoking, and drug references - this is what parents are finding on Google’s YouTube Kids app. That’s right - its kids app. Now, parents across the country are calling on Google to remove the app until it can guarantee the total elimination of this inappropriate content.\n\nWhen my neighbors told me about the horrible adult content popping up on the Youtube Kids app, I thought there must be a mistake. Why would Google market an app as “a family-friendly place to explore” and not have proper safeguards in place? Unfortunately, it turned out to be true. And I’ve since learned of the numerous complaints filed to the Federal Trade Commission about this very problem.\n\nEven worse, Google’s response has been laughable. They tell parents to simply flag inappropriate material or set new filters. As a father of two, it makes me angry when a large company like Google doesn’t take responsibility for its kids’ products. Parents are being sold on an app built for kids 5 and under that is supposed to keep them safe from adult content. Parents like myself are joining forces to hold Google accountable.\n\nTell Google to remove the YouTube Kids app until it can live up to its marketing.\n\nThe solution is simple: only allow content pre-approved for ages 5 and under to appear on the app, and don’t allow ads clearly meant for adults. Unless it can live up to expectations, the app should be removed.\n\nParents are not the only ones outraged. The media has blasted Google’s app, calling it “the most anti-family idea ever to come out of Silicon Valley,\" and reporting that it “ignores basic protections for children.”\n\nWith your support, we can get Google to remove YouTube Kids until the proper protections are in place.\n\nThese are examples of videos encountered on YouTube Kids:\n\nA graphic lecture discussing hardcore pornography by Cindy Gallop:\n\nhttps://www.youtube.com/watch?v=EgtcEq7jpAk\n\nHow to make chlorine gas with household products (chemical weapon used in Syria):\n\nhttps://www.youtube.com/watch?v=DF2CXHvh8uI\n\nHow to tie a noose:\n\nhttps://www.youtube.com/watch?v=TpAA2itjI34\n\nHow to throw knives:\n\nhttps://www.youtube.com/watch?v=NGgzn1haQ-E\n\nA guy tasting battery acid:\n\nhttps://www.youtube.com/watch?v=gif-OWNjJSw\n\nHow to use a chainsaw:\n\nhttps://www.youtube.com/watch?v=Kk28thdgCEU\n\nA “Sesame Street” episode dubbed with long strings of expletives:\n\nhttps://www.youtube.com/watch?v=kVkqzE-iiEY\n\nReferences to pedophilia in a homemade video reviewing a “My Little Pony” episode:\n\nhttps://www.youtube.com/watch?v=7K9uH4d-HnU\n\nA DIY video on conducting illegal piracy, featuring pictures of marijuana leaves:\n\nhttps://www.youtube.com/watch?v=dZDF5uqORA0", + "report_number": 10, + "source_domain": "change.org", + "submitters": ["Roman Yampolskiy"], + "tags": [], + "text": "Videos filled with profanity, sexually explicit material, alcohol, smoking, and drug references - this is what parents are finding on Google’s YouTube Kids app. That’s right - its kids app. Now, parents across the country are calling on Google to remove the app until it can guarantee the total elimination of this inappropriate content.\n\nWhen my neighbors told me about the horrible adult content popping up on the Youtube Kids app, I thought there must be a mistake. Why would Google market an app as “a family-friendly place to explore” and not have proper safeguards in place? Unfortunately, it turned out to be true. And I’ve since learned of the numerous complaints filed to the Federal Trade Commission about this very problem.\n\nEven worse, Google’s response has been laughable. They tell parents to simply flag inappropriate material or set new filters. As a father of two, it makes me angry when a large company like Google doesn’t take responsibility for its kids’ products. Parents are being sold on an app built for kids 5 and under that is supposed to keep them safe from adult content. Parents like myself are joining forces to hold Google accountable.\n\nTell Google to remove the YouTube Kids app until it can live up to its marketing.\n\nThe solution is simple: only allow content pre-approved for ages 5 and under to appear on the app, and don’t allow ads clearly meant for adults. Unless it can live up to expectations, the app should be removed.\n\nParents are not the only ones outraged. The media has blasted Google’s app, calling it “the most anti-family idea ever to come out of Silicon Valley,\" and reporting that it “ignores basic protections for children.”\n\nWith your support, we can get Google to remove YouTube Kids until the proper protections are in place.\n\nThese are examples of videos encountered on YouTube Kids:\n\nA graphic lecture discussing hardcore pornography by Cindy Gallop:\n\nhttps://www.youtube.com/watch?v=EgtcEq7jpAk\n\nHow to make chlorine gas with household products (chemical weapon used in Syria):\n\nhttps://www.youtube.com/watch?v=DF2CXHvh8uI\n\nHow to tie a noose:\n\nhttps://www.youtube.com/watch?v=TpAA2itjI34\n\nHow to throw knives:\n\nhttps://www.youtube.com/watch?v=NGgzn1haQ-E\n\nA guy tasting battery acid:\n\nhttps://www.youtube.com/watch?v=gif-OWNjJSw\n\nHow to use a chainsaw:\n\nhttps://www.youtube.com/watch?v=Kk28thdgCEU\n\nA “Sesame Street” episode dubbed with long strings of expletives:\n\nhttps://www.youtube.com/watch?v=kVkqzE-iiEY\n\nReferences to pedophilia in a homemade video reviewing a “My Little Pony” episode:\n\nhttps://www.youtube.com/watch?v=7K9uH4d-HnU\n\nA DIY video on conducting illegal piracy, featuring pictures of marijuana leaves:\n\nhttps://www.youtube.com/watch?v=dZDF5uqORA0", + "title": "Remove YouTube Kids app until it eliminates its inappropriate content", + "description": "Description Remove YouTube Kids app until it eliminates its inappropriate content", + "url": "https://www.change.org/p/remove-youtube-kids-app-until-it-eliminates-its-inappropriate-content", + "user": { + "userId": "63320ce63ec803072c9f529c" + } + } + } +} diff --git a/site/gatsby-site/playwright/fixtures/reports/reportWithTranslations.json b/site/gatsby-site/playwright/fixtures/reports/reportWithTranslations.json new file mode 100644 index 0000000000..9cdcfb4a55 --- /dev/null +++ b/site/gatsby-site/playwright/fixtures/reports/reportWithTranslations.json @@ -0,0 +1,44 @@ +{ + "data": { + "report": { + "authors": ["Marco Acevedo"], + "date_downloaded": "2019-04-13", + "date_published": "2015-07-11", + "flag": null, + "image_url": "https://assets.change.org/photos/0/yb/id/eYyBIdJOMHpqcty-1600x900-noPad.jpg?1523726975", + "report_number": 10, + "submitters": ["Roman Yampolskiy"], + "tags": ["Test Tag"], + "text": "## Video still of a reproduced version of Minnie Mouse\n\nWhich appeared on the now-suspended Simple Fun channel Simple Fun.", + "plain_text": "Video still of a reproduced version of Minnie Mouse\n\nWhich appeared on the now-suspended Simple Fun channel Simple Fun.", + "title": "Remove YouTube Kids app until it eliminates its inappropriate content", + "description": "Description YouTube Kids app until it eliminates its inappropriate content", + "url": "https://www.change.org/p/remove-youtube-kids-app-until-it-eliminates-its-inappropriate-content", + "editor_notes": "", + "language": "en", + "translations_es": { + "__typename": "ReportTranslation", + "text": "Este es un texto de prueba que tiene un largo mayor a ochenta caracteres (en español)", + "title": "Este es el Título en español" + }, + "translations_en": { + "__typename": "ReportTranslation", + "text": "", + "title": "" + }, + "translations_fr": { + "__typename": "ReportTranslation", + "text": "Il s'agit d'un texte de test de plus de quatre-vingts caractères - lorem ipsum (en français)", + "title": "C'est le Titre en français" + }, + "translations_ja": { + "__typename": "ReportTranslation", + "text": "解サオライ協立なーづ民手ぶみドに即記朝ぐ奥置ぱで地更トるあて栄厚ぜづを祭屋ん来派どてゃ読速ヘ誌約カタシネ原39業理る。外ヒヱフ社第むせゆ由更混ソエ夕野しりすよ顔飛リの兆基う公言や置17謝后嘘5供フキヌア星集ヘラ辞勘壇崇さびわ。(日本語で)", + "title": "これは日本語でのタイトルです" + }, + "date_modified": "2023-01-01", + "epoch_date_modified": 1672531200, + "is_incident_report": true + } + } +} diff --git a/site/gatsby-site/playwright/fixtures/reports/updateOneReportTranslation.json b/site/gatsby-site/playwright/fixtures/reports/updateOneReportTranslation.json new file mode 100644 index 0000000000..37906edb16 --- /dev/null +++ b/site/gatsby-site/playwright/fixtures/reports/updateOneReportTranslation.json @@ -0,0 +1,10 @@ +{ + "data": { + "updateOneReportTranslation": { + "__typename": "Report", + "report_number": 10, + "text": "Videos filled with profanity, sexually explicit material, alcohol, smoking, and drug references - this is what parents are finding on Google’s YouTube Kids app. That’s right - its kids app. Now, parents across the country are calling on Google to remove the app until it can guarantee the total elimination of this inappropriate content.\n\nWhen my neighbors told me about the horrible adult content popping up on the Youtube Kids app, I thought there must be a mistake. Why would Google market an app as “a family-friendly place to explore” and not have proper safeguards in place? Unfortunately, it turned out to be true. And I’ve since learned of the numerous complaints filed to the Federal Trade Commission about this very problem.\n\nEven worse, Google’s response has been laughable. They tell parents to simply flag inappropriate material or set new filters. As a father of two, it makes me angry when a large company like Google doesn’t take responsibility for its kids’ products. Parents are being sold on an app built for kids 5 and under that is supposed to keep them safe from adult content. Parents like myself are joining forces to hold Google accountable.\n\nTell Google to remove the YouTube Kids app until it can live up to its marketing.\n\nThe solution is simple: only allow content pre-approved for ages 5 and under to appear on the app, and don’t allow ads clearly meant for adults. Unless it can live up to expectations, the app should be removed.\n\nParents are not the only ones outraged. The media has blasted Google’s app, calling it “the most anti-family idea ever to come out of Silicon Valley,\" and reporting that it “ignores basic protections for children.”\n\nWith your support, we can get Google to remove YouTube Kids until the proper protections are in place.\n\nThese are examples of videos encountered on YouTube Kids:\n\nA graphic lecture discussing hardcore pornography by Cindy Gallop:\n\nhttps://www.youtube.com/watch?v=EgtcEq7jpAk\n\nHow to make chlorine gas with household products (chemical weapon used in Syria):\n\nhttps://www.youtube.com/watch?v=DF2CXHvh8uI\n\nHow to tie a noose:\n\nhttps://www.youtube.com/watch?v=TpAA2itjI34\n\nHow to throw knives:\n\nhttps://www.youtube.com/watch?v=NGgzn1haQ-E\n\nA guy tasting battery acid:\n\nhttps://www.youtube.com/watch?v=gif-OWNjJSw\n\nHow to use a chainsaw:\n\nhttps://www.youtube.com/watch?v=Kk28thdgCEU\n\nA “Sesame Street” episode dubbed with long strings of expletives:\n\nhttps://www.youtube.com/watch?v=kVkqzE-iiEY\n\nReferences to pedophilia in a homemade video reviewing a “My Little Pony” episode:\n\nhttps://www.youtube.com/watch?v=7K9uH4d-HnU\n\nA DIY video on conducting illegal piracy, featuring pictures of marijuana leaves:\n\nhttps://www.youtube.com/watch?v=dZDF5uqORA0", + "title": "Remove YouTube Kids app until it eliminates its inappropriate content" + } + } +} \ No newline at end of file diff --git a/site/gatsby-site/playwright/utils.ts b/site/gatsby-site/playwright/utils.ts index c5110deb97..def6b62533 100644 --- a/site/gatsby-site/playwright/utils.ts +++ b/site/gatsby-site/playwright/utils.ts @@ -3,6 +3,8 @@ import { Page, Route, test as base, Request } from '@playwright/test'; import { minimatch } from 'minimatch' import config from './config'; import assert from 'node:assert'; +import fs from 'fs'; +import path from 'path'; declare module '@playwright/test' { interface Request { @@ -171,6 +173,16 @@ const loginSteps = async (page: Page, email: string, password: string) => { await page.waitForURL(url => !url.toString().includes('/login')); }; +export async function getEditorText(page: Page, selector: string = '.CodeMirror'): Promise { + return await page.evaluate( + (selector) => { + const editor = document.querySelector(selector) as HTMLElement & { CodeMirror?: any }; + return editor?.CodeMirror ? editor.CodeMirror.getValue() : ''; + }, + selector + ); +} + export async function setEditorText(page: Page, value, selector = '.CodeMirror') { await page.locator(selector).first().click(); await page.evaluate(([value, selector]) => { @@ -178,3 +190,23 @@ export async function setEditorText(page: Page, value, selector = '.CodeMirror') }, [value, selector]); await page.mouse.click(0, 0); } + + +export async function listFiles(directoryPath: string): Promise { + return new Promise((resolve, reject) => { + fs.readdir(directoryPath, (err, files) => { + if (err) { + reject(err); + } else { + const jsonFiles = files.filter((file) => { + return ( + path.extname(file).toLowerCase() === '.json' && + fs.statSync(path.join(directoryPath, file)).isFile() + ); + }); + + resolve(jsonFiles); + } + }); + }); +} diff --git a/site/gatsby-site/src/components/cite/IncidentStatsCard.js b/site/gatsby-site/src/components/cite/IncidentStatsCard.js index 129c433895..b0538f2e94 100644 --- a/site/gatsby-site/src/components/cite/IncidentStatsCard.js +++ b/site/gatsby-site/src/components/cite/IncidentStatsCard.js @@ -65,30 +65,36 @@ const IncidentStatsCard = ({ ))} -
- Applied Taxonomies -
-
- {taxonomiesWithClassifications.map((t, i) => { - const color = { CSETv1: 'orange', GMF: 'blue' }[t] || 'gray'; + {taxonomiesWithClassifications.length > 0 && ( + <> +
+ Applied Taxonomies +
+
+ {taxonomiesWithClassifications + .filter((t) => !t.includes('_Annotator,')) + .map((t, i) => { + const color = { CSETv1: 'orange', GMF: 'blue' }[t] || 'gray'; - return ( - <> - {i > 0 && ', '} - - {t} - - - ); - })} -
+ return ( + <> + {i > 0 && ', '} + + {t} + + + ); + })} +
+ + )} ); diff --git a/site/gatsby-site/src/pages/cite/[id].js b/site/gatsby-site/src/pages/cite/[id].js index d1d06da5a5..7b5bd15926 100644 --- a/site/gatsby-site/src/pages/cite/[id].js +++ b/site/gatsby-site/src/pages/cite/[id].js @@ -21,7 +21,7 @@ function CiteDynamicPage(props) { const [incident, setIncident] = useState(null); const { data: incidentData, loading } = useQuery(FIND_FULL_INCIDENT, { - variables: { query: { incident_id } }, + variables: { query: { incident_id: parseInt(incident_id) } }, }); useEffect(() => {