#11 Added integration tests for deleting and adding questions

This commit is contained in:
Christoph Lienhard 2020-12-30 16:30:19 +01:00
parent 4ed8927522
commit acbe0a453c
Signed by: christoph.lienhard
GPG key ID: 6B98870DDC270884
7 changed files with 232 additions and 114 deletions

View file

@ -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<MockedResponse<EditQuestionResponse>> = [
@ -31,12 +41,66 @@ export const editQuestionMock: Array<MockedResponse<EditQuestionResponse>> = [
},
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<MockedResponse<AddQuestionResponse>> = [
{
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<MockedResponse<DeleteQuestionResponse>> = [
{
request: {
query: DELETE_QUESTION,
variables: deleteQuestionVariables,
},
result: {
data: {
deleteQuestion: getDeletedQuestionMock(),
}
},
},
]

View file

@ -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 {

View file

@ -2,7 +2,7 @@ import {MockedResponse} from "@apollo/client/testing";
import {BasicCategoryResponse, GET_ALL_CATEGORIES, GetAllCategoriesResponse} from "./category";
const categoryNodesMock: Array<BasicCategoryResponse> = [
export const categoryNodesMock: Array<BasicCategoryResponse> = [
{
id: "c1",
rowId: 1,

View file

@ -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(<AccordionWithEdit key={"test"} title={"Test Title"} subTitle={"Test Category"}
description="Test Description"/>);
expect(queryByText(/Test Title/)?.textContent).toBe("Test Title");
expect(queryByText(/Test Category/)?.textContent).toBe("Test Category");
expect(queryByText(/Test Description/)?.textContent).toBe("Test Description");
});
});

View file

@ -108,7 +108,6 @@ export default function QuestionList() {
cache.modify({
fields: {
allQuestions(existingQuestionsRef: { nodes: Array<Reference>} = { nodes: []}, {readField}) {
console.log("existingQuestion: ", existingQuestionsRef)
return {nodes: existingQuestionsRef.nodes.filter(questionRef => readField('id', questionRef) !== idToRemove)};
}
}

View file

@ -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<MockedResponse>) {
const initialMocks = [...getAllQuestionsMock, ...getAllCategoriesMock];
const allMocks = additionalMocks ? [...initialMocks, ...additionalMocks] : initialMocks
return render(
<MockedProvider mocks={[...getAllQuestionsMock, ...getAllCategoriesMock, ...editQuestionMock]}>
<MockedProvider mocks={allMocks}>
<MemoryRouter>
<SnackbarProvider>
<QuestionList/>
@ -33,26 +146,32 @@ const waitForInitialQuestionsToRender = async (): Promise<Array<HTMLElement>> =>
}
// sorry, I found no better way to find a specific icon button...
const queryAllEditIconsButtons = (container?: HTMLElement): Array<HTMLElement> => {
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 queryAllDeleteIconsButtons = (container?: HTMLElement): Array<HTMLElement> => {
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 = 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()
})
});
});

View file

@ -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(<DeleteIcon/>)
export const getEditIconPath = memoizedGetIconPath(<EditIcon/>)
export const getDeleteIconPath = memoizedGetIconPath(<DeleteIcon/>)
export const getAddIconPath = memoizedGetIconPath(<AddIcon/>)