diff --git a/frontend/cypress/integration/actions_spec.ts b/frontend/cypress/integration/actions_spec.ts index 7220583b8..9daf8094f 100644 --- a/frontend/cypress/integration/actions_spec.ts +++ b/frontend/cypress/integration/actions_spec.ts @@ -10,8 +10,6 @@ import { DELETE_ACTION } from '../support/gql' import * as faker from 'faker' import { getUsers } from '../support/mock/external/users' -const WAIT = 4000 // Using 4000 since the tests will fail anyways if the query takes longer than that to respond - describe('Actions', () => { const evaluationPage = new EvaluationPage() const actionsGrid = new ActionsGrid() @@ -100,6 +98,7 @@ describe('Actions', () => { editActionDialog.assertActionValues(existingAction, existingNotes) editActionDialog.editAction(updatedAction, newNotes, dropdownSelect) editActionDialog.assertSaved() + editActionDialog.assertActionValues(updatedAction, existingNotes.concat(newNotes)) editActionDialog.close() cy.testCacheAndDB(() => { evaluationPage.progressionStepLink(actionProgression).click() @@ -112,59 +111,52 @@ describe('Actions', () => { context('Complete action', () => { let seed: EvaluationSeed let editor: Participant - let existingAction: Action let existingNotes: Note[] - let updatedAction: Action - let newNote: Note - - const actionProgression = faker.random.arrayElement([Progression.Workshop, Progression.FollowUp]) beforeEach(() => { ;({ seed, editor, existingAction, existingNotes } = createEvaluationWithActionsWithNotes({ completed: false })) - ;({ updatedAction, newNote } = createCompleteActionData(seed, editor, existingAction)) - seed.plant().then(() => { - cy.visitEvaluation(seed.evaluationId, editor.user) - evaluationPage.progressionStepLink(actionProgression).click() - }) + seed.plant() }) - function testCacheInReopenedActionView() { + const actionProgression = faker.random.arrayElement([Progression.Workshop, Progression.FollowUp]) + + function openActionEditView() { + cy.visitEvaluation(seed.evaluationId, editor.user) + evaluationPage.progressionStepLink(actionProgression).click() + actionsGrid.actionLink(existingAction.questionOrder, existingAction.title).click() + } + + function testViewAfterReload(newNote: Note) { cy.testCacheAndDB(() => { evaluationPage.progressionStepLink(actionProgression).click() - actionsGrid.actionLink(existingAction.questionOrder, updatedAction.title).click() - editActionDialog.assertActionValues(updatedAction, existingNotes.concat([newNote])) + actionsGrid.actionLink(existingAction.questionOrder, existingAction.title).click() + editActionDialog.assertClosingMessageInNotes(newNote, 0) }) } - it('Action can be completed without writing a reason', () => { - actionsGrid.actionLink(existingAction.questionOrder, existingAction.title).click() - editActionDialog.assertViewActionNotCompleted() - editActionDialog.openCompleteActionView() - editActionDialog.confirmAndCheckCompleted() - cy.wait(WAIT) - editActionDialog.assertActionValues(updatedAction, existingNotes.concat([newNote])) - editActionDialog.close() - testCacheInReopenedActionView() - }) + it('Action can be completed', () => { + const saveWithReason = faker.datatype.boolean() + const newNote = createCompleteNote(existingAction, editor) + + openActionEditView() - it('Action can be completed with a reason', () => { - actionsGrid.actionLink(existingAction.questionOrder, existingAction.title).click() editActionDialog.assertViewActionNotCompleted() editActionDialog.openCompleteActionView() - editActionDialog.completedReasonInput().replace('Closed because of a good reason') + if (saveWithReason) { + editActionDialog.completedReasonInput().replace('Closed because of a good reason') + } editActionDialog.confirmAndCheckCompleted() - cy.wait(WAIT) - editActionDialog.assertActionValues(updatedAction, existingNotes.concat([newNote])) + editActionDialog.assertClosingMessageInNotes(newNote, 0) editActionDialog.close() - testCacheInReopenedActionView() + testViewAfterReload(newNote) }) it('Completing Action can be cancelled', () => { - actionsGrid.actionLink(existingAction.questionOrder, existingAction.title).click() - editActionDialog.assertViewActionNotCompleted() - editActionDialog.openCompleteActionView() + openActionEditView() + + editActionDialog.completeActionButton().click() editActionDialog.completeActionCancelButton().click() editActionDialog.assertViewActionNotCompleted() editActionDialog.assertNoClosingMessageInNotes(existingNotes) @@ -252,11 +244,6 @@ describe('Actions', () => { let actions: Action[] let editor: Participant - beforeEach(() => { - ;({ seed, editor, actions } = createViewSeed()) - seed.plant() - }) - context('Created actions are visible', () => { function getQuestion(action: Action) { return seed.questions.find(q => q.id === seed.findQuestionId(action.questionOrder))! @@ -274,18 +261,27 @@ describe('Actions', () => { } it('Actions are visible on Workshop Progression', () => { + ;({ seed, editor, actions } = createViewSeed()) + seed.plant() + cy.visitEvaluation(seed.evaluationId, editor.user) evaluationPage.progressionStepLink(Progression.Workshop).click() checkActionsPresentOnProgression() }) it('Actions are visible on Follow-Up Progression', () => { + ;({ seed, editor, actions } = createViewSeed()) + seed.plant() + cy.visitEvaluation(seed.evaluationId, editor.user) evaluationPage.progressionStepLink(Progression.FollowUp).click() checkActionsPresentOnProgression() }) it('Actions are visible on Follow-Up Actions Tab', () => { + ;({ seed, editor, actions } = createViewSeed()) + seed.plant() + cy.visitEvaluation(seed.evaluationId, editor.user) evaluationPage.progressionStepLink(Progression.FollowUp).click() actionsTab.linkToTab().click() @@ -297,6 +293,9 @@ describe('Actions', () => { }) it('Actions are visible on Dashboard Actions Tab', () => { + ;({ seed, editor, actions } = createViewSeed(true)) + seed.plant() + cy.visitProject(editor.user) dashboardActions.tab().click() actions.forEach(a => { @@ -310,56 +309,55 @@ describe('Actions', () => { context('Actions Tab on Dashboard', () => { let seed: EvaluationSeed let editor: Participant - let existingAction: Action let existingNotes: Note[] beforeEach(() => { ;({ seed, editor, existingAction, existingNotes } = createActionTableSeed()) - - seed.plant().then(() => { - cy.visitProject(editor.user) - dashboardActions.tab().click() - }) + seed.plant() }) + function openEditActionView(actionTitle: string) { + dashboardActions.tab().click() + dashboardActions.actionLink(existingAction.id, actionTitle).click() + } + it('Action can be edited', () => { const { updatedAction, newNotes } = createEditTestData(seed, editor, existingAction, false) - dashboardActions.actionLink(existingAction.id, existingAction.title).click() + cy.visitProject(editor.user) + openEditActionView(existingAction.title) + editActionDialog.assertActionValues(existingAction, existingNotes) editActionDialog.editAction(updatedAction, newNotes, dropdownSelect) editActionDialog.assertSaved() - cy.wait(WAIT) editActionDialog.assertActionValues(updatedAction, existingNotes.concat(newNotes)) editActionDialog.close() cy.testCacheAndDB(() => { - dashboardActions.tab().click() - dashboardActions.actionLink(existingAction.id, updatedAction.title).click() + openEditActionView(updatedAction.title) editActionDialog.assertActionValues(updatedAction, existingNotes.concat(newNotes)) }) }) it('Action can be completed', () => { - const saveWithComment = Math.random() < 0.5 - const { updatedAction: completedAction, newNote } = createCompleteActionData(seed, editor, existingAction) + const saveWithReason = faker.datatype.boolean() + const newNote = createCompleteNote(existingAction, editor) + + cy.visitProject(editor.user) + openEditActionView(existingAction.title) - dashboardActions.actionLink(existingAction.id, existingAction.title).click() - editActionDialog.assertViewActionNotCompleted() editActionDialog.openCompleteActionView() - if (saveWithComment) { + if (saveWithReason) { editActionDialog.completedReasonInput().replace('Closed because of a good reason') } editActionDialog.confirmAndCheckCompleted() - cy.wait(WAIT) - editActionDialog.assertActionValues(completedAction, existingNotes.concat([newNote])) + editActionDialog.assertClosingMessageInNotes(newNote, 0) editActionDialog.close() cy.testCacheAndDB(() => { - dashboardActions.tab().click() - dashboardActions.actionLink(existingAction.id, existingAction.title).click() - editActionDialog.assertActionValues(completedAction, existingNotes.concat([newNote])) + openEditActionView(existingAction.title) + editActionDialog.assertClosingMessageInNotes(newNote, 0) }) }) }) @@ -379,19 +377,18 @@ describe('Actions', () => { beforeEach(() => { ;({ seed, editor, existingAction, existingNotes } = createActionTableSeed()) - seed.plant().then(() => { - cy.visitEvaluation(seed.evaluationId, editor.user) - navigateToView(existingAction.title) - }) + seed.plant() }) it('Action can be edited', () => { const { updatedAction, newNotes } = createEditTestData(seed, editor, existingAction) + cy.visitEvaluation(seed.evaluationId, editor.user) + navigateToView(existingAction.title) + editActionDialog.assertActionValues(existingAction, existingNotes) editActionDialog.editAction(updatedAction, newNotes, dropdownSelect) editActionDialog.assertSaved() - cy.wait(WAIT) editActionDialog.assertActionValues(updatedAction, existingNotes.concat(newNotes)) editActionDialog.close() @@ -402,22 +399,23 @@ describe('Actions', () => { }) it('Action can be completed', () => { - const saveWithComment = Math.random() < 0.5 + const saveWithReason = faker.datatype.boolean() const { updatedAction: completedAction, newNote } = createCompleteActionData(seed, editor, existingAction) - editActionDialog.assertViewActionNotCompleted() + cy.visitEvaluation(seed.evaluationId, editor.user) + navigateToView(existingAction.title) + editActionDialog.openCompleteActionView() - if (saveWithComment) { + if (saveWithReason) { editActionDialog.completedReasonInput().replace('Closed because of a good reason') } editActionDialog.confirmAndCheckCompleted() - cy.wait(WAIT) - editActionDialog.assertActionValues(completedAction, existingNotes.concat([newNote])) + editActionDialog.assertClosingMessageInNotes(newNote, 0) editActionDialog.close() cy.testCacheAndDB(() => { navigateToView(completedAction.title) - editActionDialog.assertActionValues(completedAction, existingNotes.concat([newNote])) + editActionDialog.assertClosingMessageInNotes(newNote, 0) }) }) }) @@ -510,16 +508,21 @@ const createActionTableSeed = () => { return { seed, editor, existingAction, existingNotes } } -const createViewSeed = () => { +const createViewSeed = (putAllActionsOnEditor = false) => { const seed = new EvaluationSeed({ progression: faker.random.arrayElement(Object.values(Progression)), users: getUsers(faker.datatype.number({ min: 1, max: 5 })), }) const editor = faker.random.arrayElement(seed.participants) + const preassignedActionValues: Partial = { questionOrder: faker.datatype.number({ min: 1, max: 8 }) } + + if (putAllActionsOnEditor) { + preassignedActionValues.assignedTo = editor + } const actions: Action[] = Array.from({ length: faker.datatype.number({ min: 2, max: 4 }) }, () => { - return seed.createAction({ questionOrder: faker.datatype.number({ min: 1, max: 8 }), assignedTo: editor }) + return seed.createAction(preassignedActionValues) }) faker.helpers.shuffle([...actions]).forEach(a => seed.addAction(a)) @@ -562,13 +565,18 @@ const createEditTestData = (seed: EvaluationSeed, editor: Participant, existingA const createCompleteActionData = (seed: EvaluationSeed, editor: Participant, existingAction: Action) => { const updatedAction = { ...existingAction, completed: true } + const newNote = createCompleteNote(updatedAction, editor) + return { updatedAction, newNote } +} + +const createCompleteNote = (action: Action, editor: Participant) => { const newNote = new Note({ text: '', - action: updatedAction, + action: action, createdBy: editor, typeName: 'ClosingRemark', }) - return { updatedAction, newNote } + return newNote } diff --git a/frontend/cypress/support/action.ts b/frontend/cypress/support/action.ts index b79ee1a49..0b548fcec 100644 --- a/frontend/cypress/support/action.ts +++ b/frontend/cypress/support/action.ts @@ -180,6 +180,12 @@ export class EditActionDialog extends ActionDialog { }) } + assertClosingCommentInList = () => { + cy.getByDataTestid('notes_list').within(() => { + cy.getByDataTestid('closing_remark', 10000).should('exist') + }) + } + /** * Insert provided values on the Edit dialog. */ @@ -205,8 +211,14 @@ export class EditActionDialog extends ActionDialog { this.assignedToInput().should('have.value', action.assignedTo.user.name) this.priorityInput().should('have.text', mapPriority(action.priority)) this.dueDateInput().should('have.value', action.dueDate.toLocaleDateString(FUSION_DATE_LOCALE)) - this.actionCompletedText().should(action.completed ? 'exist' : 'not.exist') - this.completeActionButton().should(action.completed ? 'not.exist' : 'exist') + this.assertNoteList(notes) + } + + /** + * Check that notes list contain expected notes. + * @param notes Pass all notes in the order of creation + */ + assertNoteList = (notes: Note[]) => { ;[...notes].reverse().forEach((note, index) => { this.notesDiv().children().eq(index).as('note') if (note.__typename === 'ClosingRemark') { @@ -218,10 +230,25 @@ export class EditActionDialog extends ActionDialog { cy.get('@note').contains(note.text).should('exist') } - // TODO: time not checked. Need to figure out if it can be done reasonably + // TODO: time not checked. Need to figure out if it can be done reasonably. }) } + /** + * Check that the provided closing note is in the provided index. + */ + assertClosingMessageInNotes = (note: Note, index: number) => { + this.notesDiv().children().eq(index).as('note') + cy.get('@note').should('contain.text', note.createdBy.user.name + ' closed action') + if (note.text) { + cy.get('@note').contains(note.text).should('exist') + } + } + + /** + * Check that the notes list does not contain a closing note + * @param notes Pass all notes in the order of creation + */ assertNoClosingMessageInNotes = (notes: Note[]) => { ;[...notes].reverse().forEach((note, index) => { this.notesDiv().children().eq(index).as('note') @@ -230,6 +257,10 @@ export class EditActionDialog extends ActionDialog { }) } + /** + * Check that all expected texts and buttons are or + * are not visible when action is not completed + */ assertViewActionNotCompleted = () => { this.actionCompletedText().should('not.exist') this.completeActionButton().should('exist') @@ -238,6 +269,10 @@ export class EditActionDialog extends ActionDialog { this.completeActionCancelButton().should('not.exist') } + /** + * Check that all expected texts and buttons are or + * are not visible when the 'confirm complete'-view is open + */ openCompleteActionView = () => { this.completeActionButton().click() this.completeActionButton().should('be.disabled') @@ -246,10 +281,15 @@ export class EditActionDialog extends ActionDialog { this.completeActionCancelButton().should('exist') } + /** + * Confirm complete action and check that the view looks like + * expected afterwards + */ confirmAndCheckCompleted = () => { this.completeActionConfirmButton().click() this.assertSaved() this.actionCompletedText().should('exist') + this.assertClosingCommentInList() } } diff --git a/frontend/src/components/Action/EditForm/NotesAndClosingRemarksList.tsx b/frontend/src/components/Action/EditForm/NotesAndClosingRemarksList.tsx index 3b458129c..a26952d6c 100644 --- a/frontend/src/components/Action/EditForm/NotesAndClosingRemarksList.tsx +++ b/frontend/src/components/Action/EditForm/NotesAndClosingRemarksList.tsx @@ -42,6 +42,7 @@ const NotesAndClosingRemarksList = ({ notesAndClosingRemarks, participantsDetail return (