Skip to content

Commit

Permalink
test: Basic CRUD tests, small tidy ups and fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
DafyddLlyr committed May 28, 2024
1 parent d613be4 commit 421bf70
Show file tree
Hide file tree
Showing 3 changed files with 287 additions and 16 deletions.
15 changes: 12 additions & 3 deletions editor.planx.uk/src/@planx/components/List/Public/Context.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { createContext, ReactNode,useContext, useState } from "react";
import React, { createContext, ReactNode, useContext, useState } from "react";

import { generateNewItem,Schema, UserData } from "../model";
import { generateNewItem, Schema, UserData } from "../model";

interface ListContextValue {
schema: Schema;
Expand Down Expand Up @@ -43,7 +43,16 @@ export const ListProvider: React.FC<ListProviderProps> = ({
const editItem = (index: number) => setActiveIndex(index);

const removeItem = (index: number) => {
if (index === activeIndex || index === 0) cancelEditItem();
// If item is currently in Edit mode, exit Edit mode
if (index === activeIndex || index === 0) {
cancelEditItem();
}
// If item is before currently active card, retain active card
if (activeIndex && index < activeIndex) {
setActiveIndex((prev) => (prev === undefined ? 0 : prev - 1));
}

// Remove item from userData
setUserData((prev) => prev.filter((_, i) => i !== index));
};

Expand Down
265 changes: 262 additions & 3 deletions editor.planx.uk/src/@planx/components/List/Public/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { waitFor, within } from "@testing-library/react";
import { cloneDeep, merge } from "lodash";
import React from "react";
import { axe, setup } from "testUtils";

Expand Down Expand Up @@ -99,9 +101,266 @@ describe("Navigating back", () => {
});

describe("Building a list", () => {
test.todo("Adding an item");
test.todo("Editing an item");
test.todo("Removing an item");
it("does not display a default item if the schema has no required minimum", () => {
const mockWithMinZero = merge(cloneDeep(mockProps), { schema: { min: 0 } });
const { queryByRole, getByRole } = setup(
<ListComponent {...mockWithMinZero} />,
);

// No card present
const activeListHeading = queryByRole("heading", {
level: 2,
name: "Animal 1",
});
expect(activeListHeading).toBeNull();

// Button is present allow additional items to be added
const addItemButton = getByRole("button", {
name: /Add a new animal type/,
});
expect(addItemButton).toBeInTheDocument();
expect(addItemButton).not.toBeDisabled();
});

it("displays a default item if the schema has a required minimum", () => {
const { getByRole, queryByLabelText } = setup(
<ListComponent {...mockProps} />,
);

// Card present...
const activeListHeading = getByRole("heading", {
level: 2,
name: "Animal 1",
});
expect(activeListHeading).toBeInTheDocument();

// ...with active fields
const inputField = queryByLabelText(/What's their name?/);
expect(inputField).toBeInTheDocument();
expect(inputField).not.toBeDisabled();
});

test("Adding an item", async () => {
const { getAllByRole, getByRole, user } = setup(
<ListComponent {...mockProps} />,
);

let cards = getAllByRole("heading", { level: 2 }).map((el) =>
el.closest("div"),
);
expect(cards).toHaveLength(1);

const addItemButton = getByRole("button", {
name: /Add a new animal type/,
});
await user.click(addItemButton);

// Item successfully added
cards = getAllByRole("heading", { level: 2 }).map((el) =>
el.closest("div"),
);
expect(cards).toHaveLength(2);

// Old item is inactive
const [firstCard, secondCard] = cards;
expect(firstCard).not.toBeNull();
expect(
within(firstCard!).queryByLabelText(/What's their name?/),
).toBeNull();

// New item is active
expect(secondCard).not.toBeNull();
expect(
within(secondCard!).getByLabelText(/What's their name?/),
).toBeInTheDocument();
});

test("Editing an item", async () => {
// Setup three cards
const { getAllByRole, getByRole, user } = setup(
<ListComponent {...mockProps} />,
);

const addItemButton = getByRole("button", {
name: /Add a new animal type/,
});
await user.click(addItemButton);
await user.click(addItemButton);

let cards = getAllByRole("heading", { level: 2 }).map((el) =>
el.closest("div"),
);
expect(cards).toHaveLength(3);

let [firstCard, secondCard, thirdCard] = cards;

// Final card is currently active
expect(thirdCard).not.toBeNull();
expect(
within(thirdCard!).getByLabelText(/What's their name?/),
).toBeInTheDocument();

// Hitting "cancel" takes us out of Edit mode
const thirdCardCancelButton = within(thirdCard!).getByRole("button", {
name: /Cancel/,
});
await user.click(thirdCardCancelButton);

cards = getAllByRole("heading", { level: 2 }).map((el) =>
el.closest("div"),
);
[firstCard, secondCard, thirdCard] = cards;

// No cards currently active
expect(
within(firstCard!).queryByLabelText(/What's their name?/),
).toBeNull();
expect(
within(secondCard!).queryByLabelText(/What's their name?/),
).toBeNull();
expect(
within(thirdCard!).queryByLabelText(/What's their name?/),
).toBeNull();

// All card in view only / summary mode
expect(within(firstCard!).getByText(/What's their name?/)).toBeVisible();
expect(within(secondCard!).getByText(/What's their name?/)).toBeVisible();
expect(within(thirdCard!).getByText(/What's their name?/)).toBeVisible();

// Hit "Edit" on second card
const secondCardEditButton = within(secondCard!).getByRole("button", {
name: /Edit/,
});
await user.click(secondCardEditButton);

// Second card now editable
await waitFor(() =>
expect(
within(secondCard!).getByLabelText(/What's their name?/),
).toBeInTheDocument(),
);
});

test("Removing an item when all cards are inactive", async () => {
// Setup three cards
const { getAllByRole, getByRole, user, getByLabelText, queryAllByRole } =
setup(<ListComponent {...mockProps} />);

const addItemButton = getByRole("button", {
name: /Add a new animal type/,
});
await user.click(addItemButton);
await user.click(addItemButton);

let cards = getAllByRole("heading", { level: 2 }).map((el) =>
el.closest("div"),
);
expect(cards).toHaveLength(3);

let [firstCard, secondCard, thirdCard] = cards;

const thirdCardCancelButton = within(thirdCard!).getByRole("button", {
name: /Cancel/,
});
await user.click(thirdCardCancelButton);

[firstCard, secondCard, thirdCard] = getAllByRole("heading", {
level: 2,
}).map((el) => el.closest("div"));

// Remove third card
const thirdCardRemoveButton = within(thirdCard!).getByRole("button", {
name: /Remove/,
});
await user.click(thirdCardRemoveButton);
cards = getAllByRole("heading", { level: 2 }).map((el) =>
el.closest("div"),
);
expect(cards).toHaveLength(2);

[firstCard, secondCard] = getAllByRole("heading", { level: 2 }).map((el) =>
el.closest("div"),
);

// Previous items remain inactive
expect(
within(firstCard!).queryByLabelText(/What's their name?/),
).toBeNull();
expect(
within(secondCard!).queryByLabelText(/What's their name?/),
).toBeNull();

// Remove second card
const secondCardRemoveButton = within(secondCard!).getByRole("button", {
name: /Remove/,
});
await user.click(secondCardRemoveButton);
cards = getAllByRole("heading", { level: 2 }).map((el) =>
el.closest("div"),
);
expect(cards).toHaveLength(1);

[firstCard] = getAllByRole("heading", { level: 2 }).map((el) =>
el.closest("div"),
);

// Previous items remain inactive
expect(
within(firstCard!).queryByLabelText(/What's their name?/),
).toBeNull();

// Remove first card
const firstCardRemoveButton = within(firstCard!).getByRole("button", {
name: /Remove/,
});
await user.click(firstCardRemoveButton);
cards = queryAllByRole("heading", { level: 2 }).map((el) =>
el.closest("div"),
);
expect(cards).toHaveLength(0);

// Add item back
await user.click(addItemButton);

// This is now editable and active
const newFirstCardInput = getByLabelText(/What's their name?/);
expect(newFirstCardInput).toBeInTheDocument();
});

test("Removing an item when another card is active", async () => {
// Setup two cards
const { getAllByRole, getByRole, user, getByLabelText, queryAllByRole } =
setup(<ListComponent {...mockProps} />);

const addItemButton = getByRole("button", {
name: /Add a new animal type/,
});
await user.click(addItemButton);

const [firstCard, secondCard] = getAllByRole("heading", { level: 2 }).map(
(el) => el.closest("div"),
);

// Second card is active
expect(
within(secondCard!).getByLabelText(/What's their name?/),
).toBeInTheDocument();

// Remove first
const firstCardRemoveButton = within(firstCard!).getByRole("button", {
name: /Remove/,
});
await user.click(firstCardRemoveButton);
const cards = getAllByRole("heading", { level: 2 }).map((el) =>
el.closest("div"),
);
expect(cards).toHaveLength(1);

// First card is active
expect(
within(cards[0]!).getByLabelText(/What's their name?/),
).toBeInTheDocument();
});
});

describe("Form validation and error handling", () => {
Expand Down
23 changes: 13 additions & 10 deletions editor.planx.uk/src/@planx/components/List/Public/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import { styled } from "@mui/material/styles";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableRow from "@mui/material/TableRow";
import Typography from "@mui/material/Typography";
Expand Down Expand Up @@ -100,23 +101,25 @@ const InactiveListCard: React.FC<{
{schema.type} {index + 1}
</Typography>
<Table>
{schema.fields.map((field, i) => (
<TableRow>
<TableCell sx={{ fontWeight: FONT_WEIGHT_SEMI_BOLD }}>
{field.data.title}
</TableCell>
<TableCell>{userData[index][i]?.val}</TableCell>
</TableRow>
))}
<TableBody>
{schema.fields.map((field, i) => (
<TableRow key={`tableRow-${i}`}>
<TableCell sx={{ fontWeight: FONT_WEIGHT_SEMI_BOLD }}>
{field.data.title}
</TableCell>
<TableCell>{userData[index][i]?.val}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<Box display="flex" gap={2}>
<CardButton onClick={() => removeItem(index)}>
<DeleteIcon titleAccess="Remove" color="warning" fontSize="medium" />
<DeleteIcon color="warning" fontSize="medium" />
Remove
</CardButton>
<CardButton onClick={() => editItem(index)}>
{/* TODO: Is primary colour really right here? */}
<EditIcon titleAccess="Edit" color="primary" fontSize="medium" />
<EditIcon color="primary" fontSize="medium" />
Edit
</CardButton>
</Box>
Expand Down

0 comments on commit 421bf70

Please sign in to comment.