Skip to content

Commit

Permalink
add unit tests on makeBreadcrumbsSegments
Browse files Browse the repository at this point in the history
  • Loading branch information
enguerranws committed Sep 24, 2024
1 parent a9bed0b commit 7bd9679
Show file tree
Hide file tree
Showing 4 changed files with 244 additions and 82 deletions.
6 changes: 4 additions & 2 deletions front/src/app/components/Breadcrumbs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import { Breadcrumb } from "@codegouvfr/react-dsfr/Breadcrumb";
import { slice } from "ramda";
import React from "react";
import { errors } from "shared";
import { makeBreadcrumbsSegments } from "../contents/breadcrumbs/breadcrumbs";
import { getBreadcrumbs } from "src/app/contents/breadcrumbs/breadcrumbs";
import { useRoute } from "../routes/routes";

export const Breadcrumbs = () => {
const { name: currentRouteName } = useRoute();
if (!currentRouteName) return null;
const segments = makeBreadcrumbsSegments(currentRouteName);
const segments = getBreadcrumbs({
currentRouteKey: currentRouteName,
});
if (segments.length === 1)
throw errors.breadcrumbs.notFound({ currentRouteName });
const ancestors = slice(0, -1, segments);
Expand Down
93 changes: 13 additions & 80 deletions front/src/app/contents/breadcrumbs/breadcrumbs.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { BreadcrumbProps } from "@codegouvfr/react-dsfr/Breadcrumb";
import { flatten } from "ramda";
import { keys } from "shared";
import { makeBreadcrumbsSegments } from "src/app/utils/breadcrumbs";
import { Route } from "type-route";
import { FrontRouteKeys, FrontRouteUnion, routes } from "../../routes/routes";

Expand All @@ -12,11 +11,16 @@ export type BreadcrumbsItem = {
};
};

export type Breadcrumbs = {
[K in FrontRouteKeys]?: BreadcrumbsItem;
export type Breadcrumbs<T extends string> = {
[K in T]?: BreadcrumbsItem;
};

export const breadcrumbs: Breadcrumbs = {
export const defaultAncestor: BreadcrumbProps["segments"][0] = {
label: "Accueil",
linkProps: routes.home().link,
};

export const breadcrumbs: Breadcrumbs<FrontRouteKeys> = {
homeAgencies: {
label: "Organismes prescripteurs",
route: routes.homeAgencies(),
Expand Down Expand Up @@ -75,78 +79,7 @@ export const breadcrumbs: Breadcrumbs = {
},
};

export const makeBreadcrumbsSegments = (
currentRouteKey: FrontRouteKeys,
): BreadcrumbProps["segments"] => {
const currentRouteAncestor = keys(breadcrumbs).find((key) => {
const currentRouteInBreadcrumbs = breadcrumbs[key];
if (!currentRouteInBreadcrumbs) return false;
return (
currentRouteInBreadcrumbs.route.name === currentRouteKey ||
isRouteInChildren(currentRouteKey, currentRouteInBreadcrumbs)
);
});
return [
{
label: "Accueil",
linkProps: routes.home().link,
},
...(currentRouteAncestor && breadcrumbs[currentRouteAncestor]
? formatToBreadcrumbsSegments(
breadcrumbs[currentRouteAncestor],
currentRouteKey,
)
: []),
];
};

const isRouteInChildren = (
currentRouteKey: FrontRouteKeys,
breadcrumbsItem: BreadcrumbsItem,
): boolean => {
const children = breadcrumbsItem.children;
if (!children) return false;
return keys(children).some((key) => {
const child = children[key];
if (!child) return false;
if (child.route.name === currentRouteKey) return true;
if (child.children) {
return isRouteInChildren(currentRouteKey, child);
}
});
};

const formatToBreadcrumbsSegments = (
ancestor: BreadcrumbsItem,
currentRouteKey: FrontRouteKeys,
): BreadcrumbProps["segments"] => {
const { label, route, children } = ancestor;
const ancestorSegment = { label, linkProps: route.link };
if (!children || ancestor.route.name === currentRouteKey)
return [ancestorSegment];
const childSegments = flatten(
keys(children).map((key) => {
const child = children[key];

if (!child) return;

const { route, label, children: childChildren } = child;
const { name: routeName, link: linkProps } = route;

if (
isRouteInChildren(currentRouteKey, child) &&
routeName !== currentRouteKey &&
childChildren
) {
return formatToBreadcrumbsSegments(child, currentRouteKey);
}
if (routeName !== currentRouteKey) return;

return {
label,
linkProps,
};
}),
).filter((child) => child !== undefined);
return [ancestorSegment, ...childSegments];
};
export const getBreadcrumbs = makeBreadcrumbsSegments(
breadcrumbs,
defaultAncestor,
);
86 changes: 86 additions & 0 deletions front/src/app/utils/breadcrumbs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { BreadcrumbProps } from "@codegouvfr/react-dsfr/Breadcrumb";
import { flatten, keys } from "ramda";
import {
Breadcrumbs,
BreadcrumbsItem,
} from "src/app/contents/breadcrumbs/breadcrumbs";

export const makeBreadcrumbsSegments =
<K>(
breadcrumbsSet: Breadcrumbs<K extends string ? K : never>,
rootAncestor: BreadcrumbProps["segments"][0],
) =>
({
currentRouteKey,
}: {
currentRouteKey: K;
}): BreadcrumbProps["segments"] => {
const currentRouteAncestor = keys(breadcrumbsSet).find((key) => {
const currentRouteInBreadcrumbs = breadcrumbsSet[key];
if (!currentRouteInBreadcrumbs) return false;
return (
currentRouteInBreadcrumbs.route.name === currentRouteKey ||
isRouteInChildren(currentRouteKey, currentRouteInBreadcrumbs)
);
});
return [
rootAncestor,
...(currentRouteAncestor && breadcrumbsSet[currentRouteAncestor]
? formatToBreadcrumbsSegments(
breadcrumbsSet[currentRouteAncestor],
currentRouteKey,
)
: []),
];
};

const isRouteInChildren = <T>(
currentRouteKey: T,
breadcrumbsItem: BreadcrumbsItem,
): boolean => {
const children = breadcrumbsItem.children;
if (!children) return false;
return keys(children).some((key) => {
const child = children[key];
if (!child) return false;
if (child.route.name === currentRouteKey) return true;
if (child.children) {
return isRouteInChildren(currentRouteKey, child);
}
});
};

const formatToBreadcrumbsSegments = <T>(
ancestor: BreadcrumbsItem,
currentRouteKey: T,
): BreadcrumbProps["segments"] => {
const { label, route, children } = ancestor;
const ancestorSegment = { label, linkProps: route.link };
if (!children || ancestor.route.name === currentRouteKey)
return [ancestorSegment];
const childSegments = flatten(
keys(children).map((key) => {
const child = children[key];

if (!child) return;

const { route, label, children: childChildren } = child;
const { name: routeName, link: linkProps } = route;

if (
isRouteInChildren(currentRouteKey, child) &&
routeName !== currentRouteKey &&
childChildren
) {
return formatToBreadcrumbsSegments(child, currentRouteKey);
}
if (routeName !== currentRouteKey) return;

return {
label,
linkProps,
};
}),
).filter((child) => child !== undefined);
return [ancestorSegment, ...childSegments];
};
141 changes: 141 additions & 0 deletions front/src/app/utils/breadcrumbs.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { BreadcrumbProps } from "@codegouvfr/react-dsfr/Breadcrumb";
import { Route } from "type-route";
import { makeBreadcrumbsSegments } from "./breadcrumbs";

const makeFakeRoute = (name: string): Route<any> => ({
name,
link: {
href: `/${name}`,
onClick: () => {},
},
action: null,
params: {},
href: `/${name}`,
push: () => {},
replace: () => {},
});

describe("makeBreadcrumbsSegments", () => {
it("should returns segments for a route at level 1", () => {
const segments = getTestBreadcrumbs({
currentRouteKey: "homeCandidates",
});
expect(segments).toEqual([
{
label: "Accueil",
linkProps: {
href: "/",
onClick: expect.any(Function),
},
},
{
label: "Candidats",
linkProps: {
href: "/homeCandidates",
onClick: expect.any(Function),
},
},
]);
});
it("should returns segments for a route at a deep level", () => {
const segments = getTestBreadcrumbs({
currentRouteKey: "searchDiagoriente",
});
expect(segments).toEqual([
{
label: "Accueil",
linkProps: {
href: "/",
onClick: expect.any(Function),
},
},
{
label: "Candidats",
linkProps: {
href: "/homeCandidates",
onClick: expect.any(Function),
},
},
{
label: "Recherche",
linkProps: {
href: "/search",
onClick: expect.any(Function),
},
},
{
label: "Recherche (langage naturel)",
linkProps: {
href: "/searchDiagoriente",
onClick: expect.any(Function),
},
},
]);
});

it("should returns segments for a route without its siblings", () => {
const segments = getTestBreadcrumbs({
currentRouteKey: "beneficiaryDashboard",
});
expect(segments).toEqual([
{
label: "Accueil",
linkProps: {
href: "/",
onClick: expect.any(Function),
},
},
{
label: "Candidats",
linkProps: {
href: "/homeCandidates",
onClick: expect.any(Function),
},
},
{
label: "Tableau de bord",
linkProps: {
href: "/beneficiaryDashboard",
onClick: expect.any(Function),
},
},
]);
});
});

const testBreadcrumbsSet = {
homeCandidates: {
label: "Candidats",
route: makeFakeRoute("homeCandidates"),
children: {
search: {
label: "Recherche",
route: makeFakeRoute("search"),
children: {
searchDiagoriente: {
label: "Recherche (langage naturel)",
route: makeFakeRoute("searchDiagoriente"),
},
},
},

beneficiaryDashboard: {
label: "Tableau de bord",
route: makeFakeRoute("beneficiaryDashboard"),
},
},
},
};

const testRootAncestor: BreadcrumbProps["segments"][0] = {
label: "Accueil",
linkProps: {
href: "/",
onClick: () => {},
},
};

const getTestBreadcrumbs = makeBreadcrumbsSegments(
testBreadcrumbsSet,
testRootAncestor,
);

0 comments on commit 7bd9679

Please sign in to comment.