diff --git a/redaktions-app/src/backend/mutations/category.mock.ts b/redaktions-app/src/backend/mutations/category.mock.ts new file mode 100644 index 0000000..f8e4386 --- /dev/null +++ b/redaktions-app/src/backend/mutations/category.mock.ts @@ -0,0 +1,100 @@ +import {MockedResponse} from "@apollo/client/testing"; +import { + ADD_CATEGORY, AddCategoryResponse, + AddCategoryVariables, DELETE_CATEGORY, DeleteCategoryPayload, DeleteCategoryResponse, DeleteCategoryVariables, + EDIT_CATEGORY, EditCategoryPayload, + EditCategoryResponse, + EditCategoryVariables +} from "./category"; +import {BasicCategoryResponse} from "../queries/category"; +import {categoryNodesMock} from "../queries/category.mock"; + + +const editCategoryVariables: EditCategoryVariables = { + id: 'c1', + title: 'New title for Category 1', + description: 'Further information for C1', +}; + +const getEditedCategoryMock = (): EditCategoryPayload | null => { + const originalCategory = categoryNodesMock.find(c => c.id === editCategoryVariables.id) + return originalCategory ? { + category: { + ...originalCategory, + title: editCategoryVariables.title === undefined ? originalCategory.title : editCategoryVariables.title, + description: editCategoryVariables.description === undefined ? originalCategory.description : null, + }, + __typename: "UpdateCategoryPayload", + } : null +} + +export const editCategoryMock: Array> = [ + { + request: { + query: EDIT_CATEGORY, + variables: editCategoryVariables, + }, + result: { + data: { + updateCategory: getEditedCategoryMock(), + } + }, + }, +] + +const addCategoryVariables: AddCategoryVariables = { + title: 'New category', + description: "", +}; + +const addedCategoryMock: BasicCategoryResponse = { + id: `newC`, + rowId: 3, + title: addCategoryVariables.title as string, + description: addCategoryVariables.description as string, + __typename: "Category" +} + +export const addCategoryMock: Array> = [ + { + request: { + query: ADD_CATEGORY, + variables: addCategoryVariables, + }, + result: { + data: { + createCategory: { + category: addedCategoryMock, + __typename: "CreateCategoryPayload", + } + } + }, + }, +] + +const deleteCategoryVariables: DeleteCategoryVariables = { + id: 'c2' +}; + +const getDeletedCategoryMock = (): DeleteCategoryPayload | null => { + const categoryToBeDeleted = categoryNodesMock.find(q => q.id === deleteCategoryVariables.id) + return categoryToBeDeleted ? { + category: categoryToBeDeleted, + __typename: "DeleteCategoryPayload", + } : null +} + +export const deleteCategoryMock: Array> = [ + { + request: { + query: DELETE_CATEGORY, + variables: deleteCategoryVariables, + }, + result: { + data: { + deleteCategory: getDeletedCategoryMock(), + } + }, + }, +] + diff --git a/redaktions-app/src/backend/mutations/category.ts b/redaktions-app/src/backend/mutations/category.ts index d5b9ca2..61a84e4 100644 --- a/redaktions-app/src/backend/mutations/category.ts +++ b/redaktions-app/src/backend/mutations/category.ts @@ -13,10 +13,12 @@ export const EDIT_CATEGORY = gql` ` export interface EditCategoryResponse { - updateCategory?: { - category: BasicCategoryResponse, - __typename: "UpdateCategoryPayload", - } + updateCategory: EditCategoryPayload | null +} + +export interface EditCategoryPayload { + category: BasicCategoryResponse, + __typename: "UpdateCategoryPayload", } export interface EditCategoryVariables { @@ -37,10 +39,12 @@ export const ADD_CATEGORY = gql` ` export interface AddCategoryResponse { - createCategory?: { - category: BasicCategoryResponse, - __typename: "CreateCategoryPayload", - } + createCategory: AddCategoryPayload | null, +} + +export interface AddCategoryPayload { + category: BasicCategoryResponse, + __typename: "CreateCategoryPayload", } export interface AddCategoryVariables { @@ -60,14 +64,14 @@ export const DELETE_CATEGORY = gql` ` export interface DeleteCategoryResponse { - deleteCategory?: { - category: BasicCategoryResponse, - __typename: "DeleteCategoryPayload", - } + deleteCategory: DeleteCategoryPayload | null +} + +export interface DeleteCategoryPayload { + category: BasicCategoryResponse, + __typename: "DeleteCategoryPayload", } export interface DeleteCategoryVariables { id: string, } - - diff --git a/redaktions-app/src/backend/queries/category.mock.ts b/redaktions-app/src/backend/queries/category.mock.ts index a4c1ea4..4676687 100644 --- a/redaktions-app/src/backend/queries/category.mock.ts +++ b/redaktions-app/src/backend/queries/category.mock.ts @@ -7,13 +7,13 @@ export const categoryNodesMock: Array = [ id: "c1", rowId: 1, title: "Category 1", - description: "Further information for Q1", + description: "Further information for C1", __typename: "Category" }, { id: "c2", rowId: 2, title: "Category 2", - description: "Further information for Q2", + description: "Further information for C2", __typename: "Category" }]; diff --git a/redaktions-app/src/integration-tests/category-list.integration.test.tsx b/redaktions-app/src/integration-tests/category-list.integration.test.tsx new file mode 100644 index 0000000..711f721 --- /dev/null +++ b/redaktions-app/src/integration-tests/category-list.integration.test.tsx @@ -0,0 +1,144 @@ +import React from 'react'; +import {fireEvent, render, screen, waitFor} from '@testing-library/react' +import {MockedProvider, MockedResponse} from '@apollo/client/testing'; +import {MemoryRouter} from 'react-router-dom'; +import CategoryList from "../components/CategoryList"; +import {SnackbarProvider} from "notistack"; +import {categoryNodesMock, getAllCategoriesMock} from "../backend/queries/category.mock"; +import {addCategoryMock, deleteCategoryMock, editCategoryMock} from "../backend/mutations/category.mock"; +import {expandAccordionAndGetIconButtons, queryAllAddIconButtons, queryAllEditIconButtons} from "./test-helper"; + + +describe('The CategoryList', () => { + test('displays the existing categories, but not the details of it', async () => { + renderCategoryList(); + + const categoryCards = await waitForInitialCategoriesToRender() + categoryCards.forEach(card => { + expect(card.innerHTML).toMatch(/Category [1-2]/) + }) + expect(queryAllEditIconButtons()).toHaveLength(0) + }); + + test('enables toggling details on each category', async () => { + renderCategoryList(); + + // Initial state: Every category card is not expanded + const categoryCards = await waitForInitialCategoriesToRender() + + // Expand first category card + await expandAccordionAndGetIconButtons(categoryCards[0]) + + // Shrink first category card again + fireEvent.click(categoryCards[0]) + await waitFor(() => { + expect(queryAllEditIconButtons()).toHaveLength(0) + }); + }); + + test('enables editing a category title', async () => { + renderCategoryList(editCategoryMock); + + const categoryCards = await waitForInitialCategoriesToRender(); + const {editIconButton} = await expandAccordionAndGetIconButtons(categoryCards[0]); + + // open edit dialog + expect(screen.queryByText(/Kategorie bearbeiten/)).toBeNull(); + fireEvent.click(editIconButton); + await waitFor(() => { + expect(screen.queryByText(/Kategorie bearbeiten/)).not.toBeNull(); + }) + + // change category title + const categoryTitleField = screen.getByDisplayValue(/Category 1/); + fireEvent.change(categoryTitleField, {target: {value: "New title for Category 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(/Kategorie bearbeiten/)).toBeNull(); + expect(screen.queryByText(/New title for Category 1/)).not.toBeNull() + }) + }); + + test('enables adding a category', async () => { + renderCategoryList(addCategoryMock); + await waitForInitialCategoriesToRender(); + + // open add dialog + const dialogIdentifier = /Neue Kategorie erstellen/; + expect(screen.queryByText(dialogIdentifier)).toBeNull(); + const addButton = queryAllAddIconButtons()[0]; + fireEvent.click(addButton); + await waitFor(() => { + expect(screen.queryByText(dialogIdentifier)).not.toBeNull(); + }) + + // change category title + const categoryTitleField = screen.getByLabelText(/Zusammenfassung/); + fireEvent.change(categoryTitleField, {target: {value: "New category"}}); + await waitFor(() => { + expect(screen.queryByDisplayValue(/New category/)).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 category/)).not.toBeNull() + }) + }); + + test('enables deleting a category', async () => { + renderCategoryList(deleteCategoryMock); + + const categoryCards = await waitForInitialCategoriesToRender(); + expect(screen.queryByText(/Category 2/)).not.toBeNull(); + const {deleteIconButton} = await expandAccordionAndGetIconButtons(categoryCards[1]); + + // open delete confirmation dialog + expect(screen.queryByText(/Kategorie löschen/)).toBeNull(); + fireEvent.click(deleteIconButton); + await waitFor(() => { + expect(screen.queryByText(/Kategorie 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(/Kategorie löschen/)).toBeNull(); + expect(screen.queryByText(/Category 2/)).toBeNull(); + }) + }); +}); + +function renderCategoryList(additionalMocks?: Array) { + const initialMocks = [...getAllCategoriesMock]; + const allMocks = additionalMocks ? [...initialMocks, ...additionalMocks] : initialMocks + return render( + + + + + + + + ); +} + +const waitForInitialCategoriesToRender = async (): Promise> => { + const numberOfCategoriesInMockQuery = categoryNodesMock.length; + let categoryCards: Array = []; + await waitFor(() => { + categoryCards = screen.queryAllByRole("button", {name: /Category [1-2]/}) + expect(categoryCards.length).toEqual(numberOfCategoriesInMockQuery); + }); + return categoryCards; +} + diff --git a/redaktions-app/src/integration-tests/edit-question.integration.test.tsx b/redaktions-app/src/integration-tests/question-list.integration.test.tsx similarity index 73% rename from redaktions-app/src/integration-tests/edit-question.integration.test.tsx rename to redaktions-app/src/integration-tests/question-list.integration.test.tsx index 96dcc83..d96145d 100644 --- a/redaktions-app/src/integration-tests/edit-question.integration.test.tsx +++ b/redaktions-app/src/integration-tests/question-list.integration.test.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import {fireEvent, queryAllByRole, render, screen, waitFor} from '@testing-library/react' +import {fireEvent, render, screen, waitFor} from '@testing-library/react' import {MockedProvider, MockedResponse} from '@apollo/client/testing'; import {MemoryRouter} from 'react-router-dom'; import QuestionList from "../components/QuestionList"; @@ -7,7 +7,11 @@ import {SnackbarProvider} from "notistack"; import {getAllQuestionsMock, questionNodesMock} from "../backend/queries/question.mock"; import {getAllCategoriesMock} from "../backend/queries/category.mock"; import {addQuestionMock, deleteQuestionMock, editQuestionMock} from "../backend/mutations/question.mock"; -import {getAddIconPath, getDeleteIconPath, getEditIconPath} from "./test-helper"; +import { + expandAccordionAndGetIconButtons, + queryAllAddIconButtons, + queryAllEditIconButtons +} from "./test-helper"; describe('The QuestionList', () => { @@ -120,7 +124,6 @@ describe('The QuestionList', () => { }); }); - function renderQuestionList(additionalMocks?: Array) { const initialMocks = [...getAllQuestionsMock, ...getAllCategoriesMock]; const allMocks = additionalMocks ? [...initialMocks, ...additionalMocks] : initialMocks @@ -144,39 +147,3 @@ const waitForInitialQuestionsToRender = async (): Promise> => }); return questionCards; } - -// sorry, I found no better way to find a specific icon button... -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 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 = queryAllDeleteIconButtons(); - let deleteIconsButtons = queryAllEditIconButtons(); - expect(editIconsButtons).toHaveLength(0); - expect(deleteIconsButtons).toHaveLength(0); - fireEvent.click(accordion); - await waitFor(() => { - editIconsButtons = queryAllEditIconButtons(); - deleteIconsButtons = queryAllDeleteIconButtons(); - expect(editIconsButtons).toHaveLength(1); - expect(deleteIconsButtons).toHaveLength(1); - }) - return { - editIconButton: editIconsButtons[0], - deleteIconButton: deleteIconsButtons[0] - }; -} diff --git a/redaktions-app/src/integration-tests/test-helper.tsx b/redaktions-app/src/integration-tests/test-helper.tsx index 46b651d..c2d705d 100644 --- a/redaktions-app/src/integration-tests/test-helper.tsx +++ b/redaktions-app/src/integration-tests/test-helper.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import {render} from '@testing-library/react' +import {fireEvent, queryAllByRole, render, screen, waitFor} from '@testing-library/react' import EditIcon from '@material-ui/icons/Edit'; import DeleteIcon from '@material-ui/icons/Delete'; import AddIcon from '@material-ui/icons/Add'; @@ -22,7 +22,42 @@ const memoizedGetIconPath = (icon: JSX.Element) => { } } -export const getEditIconPath = memoizedGetIconPath() -export const getDeleteIconPath = memoizedGetIconPath() -export const getAddIconPath = memoizedGetIconPath() +const getEditIconPath = memoizedGetIconPath() +const getDeleteIconPath = memoizedGetIconPath() +const getAddIconPath = memoizedGetIconPath() +// sorry, I found no better way to find a specific icon button... +export 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 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... +export const queryAllAddIconButtons = (container?: HTMLElement): Array => { + return (container ? queryAllByRole(container, "button") : screen.queryAllByRole("button")) + .filter(button => button.innerHTML.includes("svg") && button.innerHTML.includes(getAddIconPath())); +} + +export const expandAccordionAndGetIconButtons = async (accordion: HTMLElement): Promise<{ editIconButton: HTMLElement, deleteIconButton: HTMLElement }> => { + let editIconsButtons = queryAllDeleteIconButtons(); + let deleteIconsButtons = queryAllEditIconButtons(); + expect(editIconsButtons).toHaveLength(0); + expect(deleteIconsButtons).toHaveLength(0); + fireEvent.click(accordion); + await waitFor(() => { + editIconsButtons = queryAllEditIconButtons(); + deleteIconsButtons = queryAllDeleteIconButtons(); + expect(editIconsButtons).toHaveLength(1); + expect(deleteIconsButtons).toHaveLength(1); + }) + return { + editIconButton: editIconsButtons[0], + deleteIconButton: deleteIconsButtons[0] + }; +}