Skip to content

Commit

Permalink
Merge branch 'main' of github.com:theopensystemslab/planx-new into je…
Browse files Browse the repository at this point in the history
…ss/bump-planx-core-3b39b02
  • Loading branch information
jessicamcinchak committed Jan 29, 2025
2 parents be3f026 + f239666 commit 13f8e9d
Show file tree
Hide file tree
Showing 14 changed files with 337 additions and 92 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Meta, StoryObj } from "@storybook/react";

import { ReadMePage } from "./ReadMePage";

const meta = {
title: "Design System/Pages/ReadMe",
component: ReadMePage,
} satisfies Meta<typeof ReadMePage>;

export default meta;

type Story = StoryObj<typeof meta>;

export const Basic = {
args: {
teamSlug: "barnet",
flowSlug: "Apply for prior permission",
flowInformation: {
status: "online",
description: "A long description of a service",
summary: "A short blurb",
limitations: "",
settings: {},
},
},
} satisfies Story;
50 changes: 36 additions & 14 deletions editor.planx.uk/src/pages/FlowEditor/ReadMePage/ReadMePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import Typography from "@mui/material/Typography";
import { TextInputType } from "@planx/components/TextInput/model";
import { useFormik } from "formik";
import { useToast } from "hooks/useToast";
import React from "react";
import capitalize from "lodash/capitalize";
import React, { useState } from "react";
import FlowTag from "ui/editor/FlowTag/FlowTag";
import { FlowTagType, StatusVariant } from "ui/editor/FlowTag/types";
import InputGroup from "ui/editor/InputGroup";
Expand All @@ -16,25 +17,17 @@ import SettingsSection from "ui/editor/SettingsSection";
import { CharacterCounter } from "ui/shared/CharacterCounter";
import Input from "ui/shared/Input/Input";
import InputRow from "ui/shared/InputRow";
import { Switch } from "ui/shared/Switch";
import { object, string } from "yup";

import { ExternalPortals } from "../components/Sidebar/Search/ExternalPortalList/ExternalPortals";
import { useStore } from "../lib/store";
import { FlowInformation } from "../utils";

interface ReadMePageProps {
flowInformation: FlowInformation;
teamSlug: string;
}

interface ReadMePageForm {
serviceSummary: string;
serviceDescription: string;
serviceLimitations: string;
}
import { ReadMePageForm, ReadMePageProps } from "./types";

export const ReadMePage: React.FC<ReadMePageProps> = ({
flowInformation,
teamSlug,
flowSlug,
}) => {
const { status: flowStatus } = flowInformation;
const [
Expand All @@ -44,6 +37,7 @@ export const ReadMePage: React.FC<ReadMePageProps> = ({
updateFlowSummary,
flowLimitations,
updateFlowLimitations,
externalPortals,
flowName,
] = useStore((state) => [
state.flowDescription,
Expand All @@ -52,11 +46,16 @@ export const ReadMePage: React.FC<ReadMePageProps> = ({
state.updateFlowSummary,
state.flowLimitations,
state.updateFlowLimitations,
state.externalPortals,
state.flowName,
]);

const toast = useToast();

const hasExternalPortals = Boolean(Object.keys(externalPortals).length);

const [showExternalPortals, setShowExternalPortals] = useState(false);

const formik = useFormik<ReadMePageForm>({
initialValues: {
serviceSummary: flowSummary || "",
Expand Down Expand Up @@ -129,7 +128,8 @@ export const ReadMePage: React.FC<ReadMePageProps> = ({
<Container maxWidth="formWrap">
<SettingsSection>
<Typography variant="h2" component="h3" gutterBottom>
{flowName}
{/* fallback from request params if store not populated with flowName */}
{flowName || capitalize(flowSlug.replaceAll("-", " "))}
</Typography>

<Box display={"flex"}>
Expand Down Expand Up @@ -161,6 +161,7 @@ export const ReadMePage: React.FC<ReadMePageProps> = ({
disabled={!useStore.getState().canUserEditTeam(teamSlug)}
inputProps={{
"aria-describedby": "A short blurb on what this service is.",
"aria-label": "Service Description",
}}
/>
<CharacterCounter
Expand Down Expand Up @@ -235,6 +236,27 @@ export const ReadMePage: React.FC<ReadMePageProps> = ({
</Box>
</form>
</SettingsSection>
<Box pt={2}>
<Switch
label={"Show external portals"}
name={"service.status"}
variant="editorPage"
checked={showExternalPortals}
onChange={() => setShowExternalPortals(!showExternalPortals)}
/>
{showExternalPortals &&
(hasExternalPortals ? (
<Box pt={2} data-testid="searchExternalPortalList">
<InputLegend>External Portals</InputLegend>
<Typography variant="body1" my={2}>
Your service contains the following external portals:
</Typography>
<ExternalPortals externalPortals={externalPortals} />
</Box>
) : (
<Typography>This service has no external portals.</Typography>
))}
</Box>
</Container>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { act, screen } from "@testing-library/react";
import { FullStore, useStore } from "pages/FlowEditor/lib/store";
import React from "react";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { setup } from "testUtils";
import { axe } from "vitest-axe";

import { ReadMePage } from "../ReadMePage";
import { defaultProps, longInput, platformAdminUser } from "./helpers";

const { getState, setState } = useStore;

let initialState: FullStore;

describe("Read Me Page component", () => {
beforeAll(() => (initialState = getState()));

beforeEach(() => {
getState().setUser(platformAdminUser);
});

afterEach(() => {
act(() => setState(initialState));
});

it("renders and submits data without an error", async () => {
const { user } = setup(
<DndProvider backend={HTML5Backend}>
<ReadMePage {...defaultProps} />
</DndProvider>
);

expect(getState().flowSummary).toBe("");

const serviceSummaryInput = screen.getByPlaceholderText("Description");

await user.type(serviceSummaryInput, "a summary");

await user.click(screen.getByRole("button", { name: "Save" }));

expect(screen.getByText("a summary")).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Reset changes" })); // refreshes page and refetches data

expect(getState().flowSummary).toEqual("a summary");
expect(screen.getByText("a summary")).toBeInTheDocument();
});

it("displays an error if the service description is longer than 120 characters", async () => {
const { user } = setup(
<DndProvider backend={HTML5Backend}>
<ReadMePage {...defaultProps} />
</DndProvider>
);

expect(getState().flowSummary).toBe("");

const serviceSummaryInput = screen.getByPlaceholderText("Description");

await user.type(serviceSummaryInput, longInput);

expect(
await screen.findByText("You have 2 characters too many")
).toBeInTheDocument();

await user.click(screen.getByRole("button", { name: "Save" }));

expect(
screen.getByText("Service description must be 120 characters or less")
).toBeInTheDocument();

await user.click(screen.getByRole("button", { name: "Reset changes" })); // refreshes page and refetches data
expect(getState().flowSummary).toBe(""); // db has not been updated
});

it("displays data in the fields if there is already flow information in the database", async () => {
await act(async () =>
setState({
flowSummary: "This flow summary is in the db already",
})
);

setup(
<DndProvider backend={HTML5Backend}>
<ReadMePage {...defaultProps} />
</DndProvider>
);

expect(
screen.getByText("This flow summary is in the db already")
).toBeInTheDocument();
});

it.todo("displays an error toast if there is a server-side issue"); // waiting for PR 4019 to merge first so can use msw package

it("should not have any accessibility violations", async () => {
const { container } = setup(
<DndProvider backend={HTML5Backend}>
<ReadMePage {...defaultProps} />
</DndProvider>
);

const results = await axe(container);
expect(results).toHaveNoViolations();
});

it("is not editable if the user has the teamViewer role", async () => {
const teamViewerUser = { ...platformAdminUser, isPlatformAdmin: false };
getState().setUser(teamViewerUser);

getState().setTeamMembers([{ ...teamViewerUser, role: "teamViewer" }]);

setup(
<DndProvider backend={HTML5Backend}>
<ReadMePage {...defaultProps} />
</DndProvider>
);

expect(getState().flowSummary).toBe("");

const serviceSummaryInput = screen.getByPlaceholderText("Description");

expect(serviceSummaryInput).toBeDisabled();
expect(screen.getByRole("button", { name: "Save" })).toBeDisabled();
});
});
26 changes: 26 additions & 0 deletions editor.planx.uk/src/pages/FlowEditor/ReadMePage/tests/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { ReadMePageProps } from "../types";

export const platformAdminUser = {
id: 1,
firstName: "Editor",
lastName: "Test",
isPlatformAdmin: true,
email: "test@test.com",
teams: [],
jwt: "x.y.z",
};

export const defaultProps = {
flowSlug: "apply-for-planning-permission",
teamSlug: "barnet",
flowInformation: {
status: "online",
description: "A long description of a service",
summary: "A short blurb",
limitations: "",
settings: {},
},
} as ReadMePageProps;

export const longInput =
"A wonderful serenity has taken possession of my entire soul, like these sweet mornings of spring which I enjoy with my who"; // 122 characters
13 changes: 13 additions & 0 deletions editor.planx.uk/src/pages/FlowEditor/ReadMePage/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { FlowInformation } from "../utils";

export interface ReadMePageProps {
flowInformation: FlowInformation;
teamSlug: string;
flowSlug: string;
}

export interface ReadMePageForm {
serviceSummary: string;
serviceDescription: string;
serviceLimitations: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import { setup } from "testUtils";
import { vi } from "vitest";
import { axe } from "vitest-axe";

import Search from ".";
import { flow } from "./mocks/simple";
import { VirtuosoWrapper } from "./testUtils";
import Search from "..";
import { flow } from "../mocks/simple";
import { VirtuosoWrapper } from "../testUtils";

vi.mock("react-navi", () => ({
useNavigation: () => ({
Expand All @@ -34,7 +34,7 @@ it("does not display if there are no external portals in the flow", () => {
const { queryByTestId } = setup(
<VirtuosoWrapper>
<Search />
</VirtuosoWrapper>,
</VirtuosoWrapper>
);

expect(queryByTestId("searchExternalPortalList")).not.toBeInTheDocument();
Expand All @@ -46,7 +46,7 @@ it("does not display if there is no search term provided", () => {
const { queryByTestId } = setup(
<VirtuosoWrapper>
<Search />
</VirtuosoWrapper>,
</VirtuosoWrapper>
);

expect(queryByTestId("searchExternalPortalList")).not.toBeInTheDocument();
Expand All @@ -58,14 +58,14 @@ it("displays a list of external portals if present in the flow, and a search ter
const { findByTestId, getByText, getByLabelText, user } = setup(
<VirtuosoWrapper>
<Search />
</VirtuosoWrapper>,
</VirtuosoWrapper>
);

const searchInput = getByLabelText("Search this flow and internal portals");
user.type(searchInput, "ind");

const externalPortalList = await waitFor(() =>
findByTestId("searchExternalPortalList"),
findByTestId("searchExternalPortalList")
);

expect(externalPortalList).toBeDefined();
Expand All @@ -79,14 +79,14 @@ it("allows users to navigate to the external portals", async () => {
const { getAllByRole, getByLabelText, user } = setup(
<VirtuosoWrapper>
<Search />
</VirtuosoWrapper>,
</VirtuosoWrapper>
);

const searchInput = getByLabelText("Search this flow and internal portals");
user.type(searchInput, "ind");

const [first, second] = await waitFor(
() => getAllByRole("link") as HTMLAnchorElement[],
() => getAllByRole("link") as HTMLAnchorElement[]
);
expect(first).toHaveAttribute("href", "../myTeam/portalOne");
expect(second).toHaveAttribute("href", "../myTeam/portalTwo");
Expand All @@ -98,14 +98,14 @@ it("should not have any accessibility violations on initial load", async () => {
const { container, getByLabelText, user, findByTestId } = setup(
<VirtuosoWrapper>
<Search />
</VirtuosoWrapper>,
</VirtuosoWrapper>
);

const searchInput = getByLabelText("Search this flow and internal portals");
user.type(searchInput, "ind");

await waitFor(() =>
expect(findByTestId("searchExternalPortalList")).toBeDefined(),
expect(findByTestId("searchExternalPortalList")).toBeDefined()
);

const results = await axe(container);
Expand Down
Loading

0 comments on commit 13f8e9d

Please sign in to comment.