#11 Add integration tests for displaying/deleting/editing and adding categories

This commit is contained in:
Christoph Lienhard 2020-12-30 16:51:19 +01:00
parent acbe0a453c
commit 42dc7f285d
Signed by: christoph.lienhard
GPG Key ID: 6B98870DDC270884
6 changed files with 309 additions and 59 deletions

View File

@ -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<MockedResponse<EditCategoryResponse>> = [
{
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<MockedResponse<AddCategoryResponse>> = [
{
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<MockedResponse<DeleteCategoryResponse>> = [
{
request: {
query: DELETE_CATEGORY,
variables: deleteCategoryVariables,
},
result: {
data: {
deleteCategory: getDeletedCategoryMock(),
}
},
},
]

View File

@ -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,
}

View File

@ -7,13 +7,13 @@ export const categoryNodesMock: Array<BasicCategoryResponse> = [
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"
}];

View File

@ -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<MockedResponse>) {
const initialMocks = [...getAllCategoriesMock];
const allMocks = additionalMocks ? [...initialMocks, ...additionalMocks] : initialMocks
return render(
<MockedProvider mocks={allMocks}>
<MemoryRouter>
<SnackbarProvider>
<CategoryList/>
</SnackbarProvider>
</MemoryRouter>
</MockedProvider>
);
}
const waitForInitialCategoriesToRender = async (): Promise<Array<HTMLElement>> => {
const numberOfCategoriesInMockQuery = categoryNodesMock.length;
let categoryCards: Array<HTMLElement> = [];
await waitFor(() => {
categoryCards = screen.queryAllByRole("button", {name: /Category [1-2]/})
expect(categoryCards.length).toEqual(numberOfCategoriesInMockQuery);
});
return categoryCards;
}

View File

@ -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<MockedResponse>) {
const initialMocks = [...getAllQuestionsMock, ...getAllCategoriesMock];
const allMocks = additionalMocks ? [...initialMocks, ...additionalMocks] : initialMocks
@ -144,39 +147,3 @@ const waitForInitialQuestionsToRender = async (): Promise<Array<HTMLElement>> =>
});
return questionCards;
}
// sorry, I found no better way to find a specific icon button...
const queryAllEditIconButtons = (container?: HTMLElement): Array<HTMLElement> => {
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<HTMLElement> => {
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<HTMLElement> => {
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]
};
}

View File

@ -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(<EditIcon/>)
export const getDeleteIconPath = memoizedGetIconPath(<DeleteIcon/>)
export const getAddIconPath = memoizedGetIconPath(<AddIcon/>)
const getEditIconPath = memoizedGetIconPath(<EditIcon/>)
const getDeleteIconPath = memoizedGetIconPath(<DeleteIcon/>)
const getAddIconPath = memoizedGetIconPath(<AddIcon/>)
// sorry, I found no better way to find a specific icon button...
export const queryAllEditIconButtons = (container?: HTMLElement): Array<HTMLElement> => {
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<HTMLElement> => {
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<HTMLElement> => {
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]
};
}