diff --git a/redaktions-app/src/backend/mutations/question.mock.ts b/redaktions-app/src/backend/mutations/question.mock.ts index 6250b8c..deab89d 100644 --- a/redaktions-app/src/backend/mutations/question.mock.ts +++ b/redaktions-app/src/backend/mutations/question.mock.ts @@ -1,6 +1,14 @@ import {MockedResponse} from "@apollo/client/testing"; -import {EDIT_QUESTION, EditQuestionResponse, EditQuestionVariables} from "./question"; +import { + ADD_QUESTION, AddQuestionResponse, + AddQuestionVariables, DELETE_QUESTION, DeleteQuestionPayload, DeleteQuestionResponse, DeleteQuestionVariables, + EDIT_QUESTION, EditQuestionPayload, + EditQuestionResponse, + EditQuestionVariables +} from "./question"; import {BasicQuestionResponse} from "../queries/question"; +import {questionNodesMock} from "../queries/question.mock"; +import {categoryNodesMock} from "../queries/category.mock"; const editQuestionVariables: EditQuestionVariables = { @@ -10,17 +18,19 @@ const editQuestionVariables: EditQuestionVariables = { categoryRowId: 1, }; -const editedQuestionMock: BasicQuestionResponse = { - id: editQuestionVariables.id, - title: editQuestionVariables.title as string, - description: editQuestionVariables.description as string, - categoryByCategoryRowId: { - id: "c1", - rowId: editQuestionVariables.categoryRowId as number, - title: "Category 1", - __typename: "Category" - }, - __typename: "Question" +const getEditedQuestionMock = (): EditQuestionPayload | null => { + const originalQuestion = questionNodesMock.find(q => q.id === editQuestionVariables.id) + return originalQuestion ? { + question: { + ...originalQuestion, + title: editQuestionVariables.title === undefined ? originalQuestion.title : editQuestionVariables.title, + description: editQuestionVariables.description === undefined ? originalQuestion.description : null, + categoryByCategoryRowId: editQuestionVariables.categoryRowId === undefined + ? originalQuestion.categoryByCategoryRowId + : categoryNodesMock.find(c => c.rowId === editQuestionVariables.categoryRowId) || null, + }, + __typename: "UpdateQuestionPayload", + } : null } export const editQuestionMock: Array> = [ @@ -31,12 +41,66 @@ export const editQuestionMock: Array> = [ }, result: { data: { - updateQuestion: { - question: editedQuestionMock, - __typename: "UpdateQuestionPayload", + updateQuestion: getEditedQuestionMock(), + } + }, + }, +] + +const addQuestionVariables: AddQuestionVariables = { + title: 'New question?', + description: "", + categoryRowId: null, +}; + +const addedQuestionMock: BasicQuestionResponse = { + id: `newQ`, + title: addQuestionVariables.title as string, + description: addQuestionVariables.description as string, + categoryByCategoryRowId: categoryNodesMock.find(c => c.rowId === editQuestionVariables.categoryRowId) || null, + __typename: "Question" +} + +export const addQuestionMock: Array> = [ + { + request: { + query: ADD_QUESTION, + variables: addQuestionVariables, + }, + result: { + data: { + createQuestion: { + question: addedQuestionMock, + __typename: "CreateQuestionPayload", } } }, }, ] +const deleteQuestionVariables: DeleteQuestionVariables = { + id: 'q2' +}; + +const getDeletedQuestionMock = (): DeleteQuestionPayload | null => { + const questionToBeDeleted = questionNodesMock.find(q => q.id === deleteQuestionVariables.id) + return questionToBeDeleted ? { + question: questionToBeDeleted, + __typename: "DeleteQuestionPayload", + } : null +} + +export const deleteQuestionMock: Array> = [ + { + request: { + query: DELETE_QUESTION, + variables: deleteQuestionVariables, + }, + result: { + data: { + deleteQuestion: getDeletedQuestionMock(), + } + }, + }, +] + diff --git a/redaktions-app/src/backend/mutations/question.ts b/redaktions-app/src/backend/mutations/question.ts index 0c2210e..ab4853c 100644 --- a/redaktions-app/src/backend/mutations/question.ts +++ b/redaktions-app/src/backend/mutations/question.ts @@ -13,10 +13,12 @@ export const EDIT_QUESTION = gql` ` export interface EditQuestionResponse { - updateQuestion?: { - question: BasicQuestionResponse, - __typename: "UpdateQuestionPayload", - }, + updateQuestion: EditQuestionPayload | null, +} + +export interface EditQuestionPayload { + question: BasicQuestionResponse, + __typename: "UpdateQuestionPayload", } export interface EditQuestionVariables { @@ -38,10 +40,12 @@ export const ADD_QUESTION = gql` ` export interface AddQuestionResponse { - createQuestion?: { - question: BasicQuestionResponse, - __typename: "CreateQuestionPayload", - } + createQuestion: AddQuestionPayload | null +} + +export interface AddQuestionPayload { + question: BasicQuestionResponse, + __typename: "CreateQuestionPayload", } export interface AddQuestionVariables { @@ -62,10 +66,12 @@ export const DELETE_QUESTION = gql` ` export interface DeleteQuestionResponse { - deleteQuestion?: { - question: BasicQuestionResponse - __typename: "DeleteQuestionPayload", - } + deleteQuestion: DeleteQuestionPayload | null, +} + +export interface DeleteQuestionPayload { + question: BasicQuestionResponse, + __typename: "DeleteQuestionPayload", } export interface DeleteQuestionVariables { diff --git a/redaktions-app/src/backend/queries/category.mock.ts b/redaktions-app/src/backend/queries/category.mock.ts index 74f5c74..a4c1ea4 100644 --- a/redaktions-app/src/backend/queries/category.mock.ts +++ b/redaktions-app/src/backend/queries/category.mock.ts @@ -2,7 +2,7 @@ import {MockedResponse} from "@apollo/client/testing"; import {BasicCategoryResponse, GET_ALL_CATEGORIES, GetAllCategoriesResponse} from "./category"; -const categoryNodesMock: Array = [ +export const categoryNodesMock: Array = [ { id: "c1", rowId: 1, diff --git a/redaktions-app/src/components/Question.test.tsx b/redaktions-app/src/components/Question.test.tsx deleted file mode 100644 index 4051686..0000000 --- a/redaktions-app/src/components/Question.test.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import {render} from '@testing-library/react' -import AccordionWithEdit from "./AccordionWithEdit"; - - -describe('The Question component', () => { - test('displays title, category and description', () => { - const {queryByText} = render(); - expect(queryByText(/Test Title/)?.textContent).toBe("Test Title"); - expect(queryByText(/Test Category/)?.textContent).toBe("Test Category"); - expect(queryByText(/Test Description/)?.textContent).toBe("Test Description"); - }); -}); diff --git a/redaktions-app/src/components/QuestionList.tsx b/redaktions-app/src/components/QuestionList.tsx index da8a79c..79398d3 100644 --- a/redaktions-app/src/components/QuestionList.tsx +++ b/redaktions-app/src/components/QuestionList.tsx @@ -108,7 +108,6 @@ export default function QuestionList() { cache.modify({ fields: { allQuestions(existingQuestionsRef: { nodes: Array} = { nodes: []}, {readField}) { - console.log("existingQuestion: ", existingQuestionsRef) return {nodes: existingQuestionsRef.nodes.filter(questionRef => readField('id', questionRef) !== idToRemove)}; } } diff --git a/redaktions-app/src/integration-tests/edit-question.integration.test.tsx b/redaktions-app/src/integration-tests/edit-question.integration.test.tsx index 957a5f3..96dcc83 100644 --- a/redaktions-app/src/integration-tests/edit-question.integration.test.tsx +++ b/redaktions-app/src/integration-tests/edit-question.integration.test.tsx @@ -1,18 +1,131 @@ import React from 'react'; import {fireEvent, queryAllByRole, render, screen, waitFor} from '@testing-library/react' -import {MockedProvider} from '@apollo/client/testing'; +import {MockedProvider, MockedResponse} from '@apollo/client/testing'; import {MemoryRouter} from 'react-router-dom'; import QuestionList from "../components/QuestionList"; import {SnackbarProvider} from "notistack"; import {getAllQuestionsMock, questionNodesMock} from "../backend/queries/question.mock"; import {getAllCategoriesMock} from "../backend/queries/category.mock"; -import {editQuestionMock} from "../backend/mutations/question.mock"; -import {getDeleteIconPath, getEditIconPath} from "./test-helper"; +import {addQuestionMock, deleteQuestionMock, editQuestionMock} from "../backend/mutations/question.mock"; +import {getAddIconPath, getDeleteIconPath, getEditIconPath} from "./test-helper"; -function renderQuestionList() { +describe('The QuestionList', () => { + test('displays the existing questions, but not the details of it', async () => { + renderQuestionList(); + + const questionCards = await waitForInitialQuestionsToRender() + questionCards.forEach(card => { + expect(card.innerHTML).toMatch(/Question [1-3]\?/) + }) + expect(questionCards[0].innerHTML).toMatch(/Category 1/); + expect(queryAllEditIconButtons()).toHaveLength(0) + }); + + test('enables toggling details on each question', async () => { + renderQuestionList(); + + // Initial state: Every question card is not expanded + const questionCards = await waitForInitialQuestionsToRender() + + // Expand first question card + await expandAccordionAndGetIconButtons(questionCards[0]) + + // Shrink first question card again + fireEvent.click(questionCards[0]) + await waitFor(() => { + expect(queryAllEditIconButtons()).toHaveLength(0) + }); + }); + + test('enables editing a question title', async () => { + renderQuestionList(editQuestionMock); + + const questionCards = await waitForInitialQuestionsToRender(); + const {editIconButton} = await expandAccordionAndGetIconButtons(questionCards[0]); + + // open edit dialog + expect(screen.queryByText(/Frage bearbeiten/)).toBeNull(); + fireEvent.click(editIconButton); + await waitFor(() => { + expect(screen.queryByText(/Frage bearbeiten/)).not.toBeNull(); + }) + + // change question title + const questionTitleField = screen.getByDisplayValue(/Question 1/); + fireEvent.change(questionTitleField, {target: {value: "New title for Question 1?"}}); + await waitFor(() => { + expect(screen.queryByDisplayValue(/New title for /)).not.toBeNull(); + }) + + // call backend and assert apollo cache update + const confirmButton = screen.getByRole("button", {name: /Speichern/}); + fireEvent.click(confirmButton); + await waitFor(() => { + expect(screen.queryByText(/Frage bearbeiten/)).toBeNull(); + expect(screen.queryByText(/New title for Question 1/)).not.toBeNull() + }) + }); + + test('enables adding a question', async () => { + renderQuestionList(addQuestionMock); + await waitForInitialQuestionsToRender(); + + // open add dialog + const dialogIdentifier = /Neue Frage erstellen/; + expect(screen.queryByText(dialogIdentifier)).toBeNull(); + const addButton = queryAllAddIconButtons()[0]; + fireEvent.click(addButton); + await waitFor(() => { + expect(screen.queryByText(dialogIdentifier)).not.toBeNull(); + }) + + // change question title + const questionTitleField = screen.getByLabelText(/Zusammenfassung/); + fireEvent.change(questionTitleField, {target: {value: "New question?"}}); + await waitFor(() => { + expect(screen.queryByDisplayValue(/New question/)).not.toBeNull(); + }) + + // call backend and assert apollo cache update + const confirmButton = screen.getByRole("button", {name: /Erstellen/}); + fireEvent.click(confirmButton); + await waitFor(() => { + expect(screen.queryByText(dialogIdentifier)).toBeNull(); + expect(screen.queryByText(/New question/)).not.toBeNull() + }) + }); + + test('enables deleting a question', async () => { + renderQuestionList(deleteQuestionMock); + + const questionCards = await waitForInitialQuestionsToRender(); + expect(screen.queryByText(/Question 2/)).not.toBeNull(); + const {deleteIconButton} = await expandAccordionAndGetIconButtons(questionCards[1]); + + // open delete confirmation dialog + expect(screen.queryByText(/Frage löschen/)).toBeNull(); + fireEvent.click(deleteIconButton); + await waitFor(() => { + expect(screen.queryByText(/Frage löschen/)).not.toBeNull(); + }) + + // call backend and assert apollo cache update + const confirmButton = screen.getByRole("button", {name: /Löschen/}); + fireEvent.click(confirmButton); + await waitFor(() => { + expect(screen.queryByText(/Frage löschen/)).toBeNull(); + expect(screen.queryByText(/Question 2/)).toBeNull(); + }) + }); +}); + + +function renderQuestionList(additionalMocks?: Array) { + const initialMocks = [...getAllQuestionsMock, ...getAllCategoriesMock]; + const allMocks = additionalMocks ? [...initialMocks, ...additionalMocks] : initialMocks return render( - + @@ -33,26 +146,32 @@ const waitForInitialQuestionsToRender = async (): Promise> => } // sorry, I found no better way to find a specific icon button... -const queryAllEditIconsButtons = (container?: HTMLElement): Array => { +const queryAllEditIconButtons = (container?: HTMLElement): Array => { return (container ? queryAllByRole(container, "button") : screen.queryAllByRole("button")) .filter(button => button.innerHTML.includes("svg") && button.innerHTML.includes(getEditIconPath())); } // sorry, I found no better way to find a specific icon button... -const queryAllDeleteIconsButtons = (container?: HTMLElement): Array => { +const queryAllDeleteIconButtons = (container?: HTMLElement): Array => { return (container ? queryAllByRole(container, "button") : screen.queryAllByRole("button")) .filter(button => button.innerHTML.includes("svg") && button.innerHTML.includes(getDeleteIconPath())); } +// sorry, I found no better way to find a specific icon button... +const queryAllAddIconButtons = (container?: HTMLElement): Array => { + return (container ? queryAllByRole(container, "button") : screen.queryAllByRole("button")) + .filter(button => button.innerHTML.includes("svg") && button.innerHTML.includes(getAddIconPath())); +} + const expandAccordionAndGetIconButtons = async (accordion: HTMLElement): Promise<{ editIconButton: HTMLElement, deleteIconButton: HTMLElement }> => { - let editIconsButtons = queryAllDeleteIconsButtons(); - let deleteIconsButtons = queryAllEditIconsButtons(); + let editIconsButtons = queryAllDeleteIconButtons(); + let deleteIconsButtons = queryAllEditIconButtons(); expect(editIconsButtons).toHaveLength(0); expect(deleteIconsButtons).toHaveLength(0); fireEvent.click(accordion); await waitFor(() => { - editIconsButtons = queryAllEditIconsButtons(); - deleteIconsButtons = queryAllDeleteIconsButtons(); + editIconsButtons = queryAllEditIconButtons(); + deleteIconsButtons = queryAllDeleteIconButtons(); expect(editIconsButtons).toHaveLength(1); expect(deleteIconsButtons).toHaveLength(1); }) @@ -61,61 +180,3 @@ const expandAccordionAndGetIconButtons = async (accordion: HTMLElement): Promise deleteIconButton: deleteIconsButtons[0] }; } - -describe('The QuestionList', () => { - test('displays the existing questions, but not the details of it', async () => { - renderQuestionList(); - - const questionCards = await waitForInitialQuestionsToRender() - questionCards.forEach(card => { - expect(card.innerHTML).toMatch(/Question [1-3]\?/) - }) - expect(questionCards[0].innerHTML).toMatch(/Category 1/); - expect(queryAllEditIconsButtons()).toHaveLength(0) - }); - - test('enables toggling details on each question', async () => { - renderQuestionList(); - - // Initial state: Every question card is not expanded - const questionCards = await waitForInitialQuestionsToRender() - - // Expand first question card - await expandAccordionAndGetIconButtons(questionCards[0]) - - // Shrink first question card again - fireEvent.click(questionCards[0]) - await waitFor(() => { - expect(queryAllEditIconsButtons()).toHaveLength(0) - }); - }); - - test('enables editing a question title', async () => { - renderQuestionList(); - - const questionCards = await waitForInitialQuestionsToRender(); - const {editIconButton} = await expandAccordionAndGetIconButtons(questionCards[0]); - - // open edit dialog - expect(screen.queryByText(/Frage bearbeiten/)).toBeNull(); - fireEvent.click(editIconButton); - await waitFor(() => { - expect(screen.queryByText(/Frage bearbeiten/)).not.toBeNull(); - }) - - // change question title - const questionTitleField = screen.getByDisplayValue(/Question 1/); - fireEvent.change(questionTitleField, {target: {value: "New title for Question 1?"}}); - await waitFor(() => { - expect(screen.queryByDisplayValue(/New title for /)).not.toBeNull(); - }) - const confirmButton = screen.getByRole("button", {name: /Speichern/}); - - // call backend and assert apollo cache update - fireEvent.click(confirmButton); - await waitFor(() => { - expect(screen.queryByText(/Frage bearbeiten/)).toBeNull(); - expect(screen.queryByText(/New title for Question 1/)).not.toBeNull() - }) - }); -}); diff --git a/redaktions-app/src/integration-tests/test-helper.tsx b/redaktions-app/src/integration-tests/test-helper.tsx index 2799aca..46b651d 100644 --- a/redaktions-app/src/integration-tests/test-helper.tsx +++ b/redaktions-app/src/integration-tests/test-helper.tsx @@ -1,7 +1,8 @@ import React from 'react'; import {render} from '@testing-library/react' -import DeleteIcon from '@material-ui/icons/Delete'; import EditIcon from '@material-ui/icons/Edit'; +import DeleteIcon from '@material-ui/icons/Delete'; +import AddIcon from '@material-ui/icons/Add'; const memoizedGetIconPath = (icon: JSX.Element) => { @@ -21,6 +22,7 @@ const memoizedGetIconPath = (icon: JSX.Element) => { } } -export const getDeleteIconPath = memoizedGetIconPath() export const getEditIconPath = memoizedGetIconPath() +export const getDeleteIconPath = memoizedGetIconPath() +export const getAddIconPath = memoizedGetIconPath()