Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
226 changes: 226 additions & 0 deletions src/components/ComparisonDependencies.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
import { describe, it, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import ComparisonDependencies from "./ComparisonDependencies";
import type { DependencyRow } from "./DependenciesTable";
import type { PathwayMetadataType } from "../types";
import pathwayMetadataSchema from "../schema/pathwayMetadata.v2.json" with { type: "json" };

const dependency = (overrides: Partial<DependencyRow> = {}): DependencyRow => ({
dependency_name: "Policy strategy",
dependency_description: "Relies on a national carbon price.",
sector: "Power",
evidence_type: "Qualitative",
...overrides,
});

const pathway = (
id: string,
dependencies: DependencyRow[],
): PathwayMetadataType =>
({
id,
name: { full: `Pathway ${id}`, short: id },
dependencies,
}) as unknown as PathwayMetadataType;

const rowLabels = () =>
screen
.getAllByText(
(_, el) =>
el?.tagName === "P" &&
el.className.includes("uppercase") &&
el.className.includes("font-semibold"),
)
.map((el) => el.textContent);

describe("ComparisonDependencies", () => {
it("lists every dependency category from the schema, in schema order", () => {
render(
<ComparisonDependencies
pathways={[pathway("a", [dependency()]), pathway("b", [dependency()])]}
sector="Power"
/>,
);

expect(rowLabels()).toEqual(
pathwayMetadataSchema.properties.dependencies.items.properties
.dependency_name.enum,
);
});

it("shows each pathway's evidence and description under its category", () => {
render(
<ComparisonDependencies
pathways={[
pathway("a", [dependency()]),
pathway("b", [
dependency({
dependency_name: "Technology",
dependency_description: "Assumes CCUS at scale.",
evidence_type: "Anecdotal",
}),
]),
]}
sector="Power"
/>,
);

expect(
screen.getByText("Relies on a national carbon price."),
).toBeInTheDocument();
expect(screen.getByText("Qualitative")).toBeInTheDocument();
expect(screen.getByText("Assumes CCUS at scale.")).toBeInTheDocument();
expect(screen.getByText("Anecdotal")).toBeInTheDocument();
});

it("says when a pathway records nothing under a category", () => {
render(
<ComparisonDependencies
pathways={[pathway("a", [dependency()]), pathway("b", [dependency()])]}
sector="Power"
/>,
);

// Two columns, ten categories, one recorded in each column.
expect(screen.getAllByText("No information")).toHaveLength(18);
});

it("shows only the selected sector's dependencies", () => {
render(
<ComparisonDependencies
pathways={[
pathway("a", [
dependency(),
dependency({
sector: "Steel",
dependency_description: "Needs green hydrogen.",
}),
]),
pathway("b", [
dependency({
sector: "Steel",
dependency_description: "Needs scrap supply.",
}),
]),
]}
sector="Steel"
/>,
);

expect(screen.getByText("Needs green hydrogen.")).toBeInTheDocument();
expect(screen.getByText("Needs scrap supply.")).toBeInTheDocument();
expect(
screen.queryByText("Relies on a national carbon price."),
).not.toBeInTheDocument();
});

it("stacks a category a pathway records more than once", () => {
render(
<ComparisonDependencies
pathways={[
pathway("a", [
dependency(),
dependency({ dependency_description: "Also needs grid reform." }),
]),
pathway("b", [dependency()]),
]}
sector="Power"
/>,
);

expect(screen.getByText("Also needs grid reform.")).toBeInTheDocument();
expect(
screen.getAllByText("Relies on a national carbon price."),
).toHaveLength(2);
});

it("explains a column with nothing for the sector once, leaving its cells blank", () => {
render(
<ComparisonDependencies
pathways={[
pathway("a", [dependency()]),
pathway("b", [dependency({ sector: "Steel" })]),
]}
sector="Power"
/>,
);

expect(
screen.getAllByText(
"This pathway has no dependencies recorded for Power.",
),
).toHaveLength(1);
// Only column a's nine unrecorded categories say so; column b stays blank.
expect(screen.getAllByText("No information")).toHaveLength(9);
expect(rowLabels()).toHaveLength(10);
});

it("leaves out the column note when every column has something", () => {
render(
<ComparisonDependencies
pathways={[pathway("a", [dependency()]), pathway("b", [dependency()])]}
sector="Power"
/>,
);

expect(
screen.queryByText(/has no dependencies recorded/),
).not.toBeInTheDocument();
});

it("shows one message instead of empty rows when no pathway has any", () => {
render(
<ComparisonDependencies
pathways={[pathway("a", [dependency()]), pathway("b", [])]}
sector="Steel"
/>,
);

expect(
screen.getByText("None of these pathways record dependencies for Steel."),
).toBeInTheDocument();
expect(screen.queryByText("Policy strategy")).not.toBeInTheDocument();
});

it("handles pathways without a dependencies list", () => {
render(
<ComparisonDependencies
pathways={[
{ id: "a" } as unknown as PathwayMetadataType,
pathway("b", [dependency()]),
]}
sector="Power"
/>,
);

expect(
screen.getByText("This pathway has no dependencies recorded for Power."),
).toBeInTheDocument();
});

it("explains an empty section when there is no sector to scope by", () => {
render(
<ComparisonDependencies
pathways={[pathway("a", [dependency()]), pathway("b", [dependency()])]}
sector={null}
/>,
);

expect(
screen.getByText(
"No dependencies are recorded for the sectors these pathways share.",
),
).toBeInTheDocument();
});

it("renders nothing without pathways", () => {
const { container } = render(
<ComparisonDependencies
pathways={[]}
sector="Power"
/>,
);

expect(container).toBeEmptyDOMElement();
});
});
153 changes: 153 additions & 0 deletions src/components/ComparisonDependencies.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import React from "react";
import { PathwayMetadataType } from "../types";
import Badge from "./Badge";
import { DependencyRow, EvidenceBadge } from "./DependenciesTable";

/**
* Every dependency category, in schema order. Listed here rather than derived
* from the data so each row renders for every comparison, keeping the columns
* aligned; the test pins the rendered rows to the schema's enum.
*/
const DEPENDENCY_NAMES: readonly DependencyRow["dependency_name"][] = [
"Policy strategy",
"Regulatory framework",
"Market and economics",
"Public acceptance",
"Consumer and client behavior",
"Infrastructure and logistics",
"Technology",
"Resource availability",
"Environmental impacts and ecosystem services",
"Labor availability",
];

interface ComparisonDependenciesProps {
pathways: PathwayMetadataType[];
/**
* The sector from the scope header. Dependencies are recorded per sector, so
* this is what scopes them; null means no shared sector has anything to show.
* There is no geography prop: dependencies are not scoped by geography.
*/
sector: string | null;
}

const EmptyState: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<div className="rounded-lg border border-neutral-200 bg-neutral-50 p-6 text-rmigray-600">
<p className="text-sm">{children}</p>
</div>
);

/**
* The comparison page's "Dependencies" section: one row per dependency
* category, labelled once across the columns like `ComparisonKeyFeatures`, with
* each pathway's evidence and description underneath.
*
* Every category renders, recorded or not, so the rows line up across columns.
* A pathway that records nothing for the selected sector gets one note at the
* top of its column instead of ten "No information" cells, which would read as
* ten separate gaps rather than one.
*/
const ComparisonDependencies: React.FC<ComparisonDependenciesProps> = ({
pathways,
sector,
}) => {
const n = pathways.length;
if (n === 0) return null;

if (sector === null) {
return (
<EmptyState>
No dependencies are recorded for the sectors these pathways share.
</EmptyState>
);
}

const rowsByPathway = pathways.map((pathway) =>
(pathway.dependencies ?? []).filter((d) => d.sector === sector),
);

if (rowsByPathway.every((rows) => rows.length === 0)) {
return (
<EmptyState>
{`None of these pathways record dependencies for ${sector}.`}
</EmptyState>
);
}

const hasEmptyColumn = rowsByPathway.some((rows) => rows.length === 0);

return (
<div
className="grid gap-x-6"
style={{ gridTemplateColumns: `repeat(${n}, 1fr)` }}
>
{hasEmptyColumn &&
pathways.map((pathway, idx) => (
<div
key={pathway.id}
className="pb-3 min-w-0"
>
{rowsByPathway[idx].length === 0 && (
<p className="text-sm italic text-rmigray-600">
{`This pathway has no dependencies recorded for ${sector}.`}
</p>
)}
</div>
))}

{DEPENDENCY_NAMES.map((name, rowIdx) => (
<React.Fragment key={name}>
{/* Single label above all pathway cells */}
<div
className="pt-3 pb-1"
style={{ gridColumn: "1 / -1" }}
>
<p className="text-xs font-semibold text-rmigray-500 uppercase tracking-wide">
{name}
</p>
</div>

{pathways.map((pathway, idx) => {
const rows = rowsByPathway[idx];
const matches = rows.filter((d) => d.dependency_name === name);
return (
<div
key={pathway.id}
className="pb-3 min-w-0"
>
{/* Blank under the column note: the note already explains it. */}
{rows.length === 0 ? null : matches.length === 0 ? (
<Badge>No information</Badge>
) : (
// The schema forbids only exact duplicates, so a pathway can
// record one category more than once; each one stacks.
matches.map((d, i) => (
<div
key={i}
className={i > 0 ? "mt-3" : undefined}
>
<EvidenceBadge value={d.evidence_type} />
<p className="mt-1.5 text-sm text-rmigray-700">
{d.dependency_description}
</p>
</div>
))
)}
</div>
);
})}

{/* Separator between rows, not after the last one */}
{rowIdx < DEPENDENCY_NAMES.length - 1 && (
<div
className="border-b border-neutral-200"
style={{ gridColumn: "1 / -1" }}
/>
)}
</React.Fragment>
))}
</div>
);
};

export default ComparisonDependencies;
Loading
Loading