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
136 changes: 136 additions & 0 deletions frontend/src/features/match/MatchView.nearMissTolerance.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { fireEvent, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { http, HttpResponse } from "msw";
import { MemoryRouter } from "react-router-dom";
import { describe, expect, it } from "vitest";
import { server } from "../../test/msw/server";
import type { OkhManifest } from "../../types/okh";
import type { NetworkData } from "../../api/ohm/network";
import { MatchView } from "./MatchView";

const design: OkhManifest = {
id: "design-1",
title: "Widget",
version: null,
repo: null,
function: null,
description: null,
intended_use: null,
keywords: [],
documentation_language: null,
license: null,
licensor: { name: "Acme" } as OkhManifest["licensor"],
contributors: [],
manufacturing_processes: [],
materials: [],
design_files: [],
manufacturing_files: [],
making_instructions: [],
parts: [],
tool_list: [],
image: null,
project_link: null,
};

const seededNetwork: NetworkData = {
spaces: [
{
id: "facility-1",
name: "Alpha Lab",
lat: 45.5,
lon: -122.6,
source: "local",
city: "Portland",
region: "OR",
country: "US",
status: null,
processes: [],
access_type: null,
url: null,
},
],
total: 1,
local_count: 1,
mom_count: 0,
dropped_no_coords: 0,
mom_available: false,
};

function renderView() {
const client = new QueryClient({
defaultOptions: { queries: { retry: false, staleTime: Infinity } },
});
client.setQueryData(["network", "baseline"], seededNetwork);
client.setQueryData(["okh-list"], {
items: [design],
pagination: {
page: 1,
page_size: 1,
total_items: 1,
total_pages: 1,
has_next: false,
has_previous: false,
},
});
return render(
<QueryClientProvider client={client}>
<MemoryRouter>
<MatchView okhId="design-1" />
</MemoryRouter>
</QueryClientProvider>,
);
}

describe("MatchView — near-miss tolerance", () => {
it("surfaces the tolerance slider instead of a blank empty state when every facility is hidden by default tolerance", async () => {
// 5 requirements, 3 missing: exceeds the default tolerance of 1, so the
// one and only solution is hidden by `withinTolerance` — but the API DID
// return a match, so this must not render the "zero solutions" empty state.
server.use(
http.post("*/v1/api/match", () =>
HttpResponse.json({
data: {
solutions: [
{
facility_id: "facility-1",
facility_name: "Alpha Lab",
confidence: 0.4,
score: 0.4,
rank: 1,
match_type: "manufacturing",
explanation: {
requirement_matches: [
{ requirement_value: "cnc milling", status: "matched" },
{ requirement_value: "welding", status: "matched" },
{ requirement_value: "anodizing", status: "not_matched" },
{ requirement_value: "laser cutting", status: "not_matched" },
{ requirement_value: "3d printing", status: "not_matched" },
],
},
},
],
total_solutions: 1,
},
}),
),
);

const user = userEvent.setup();
renderView();

await user.click(await screen.findByLabelText("Alpha Lab"));
await user.click(screen.getByRole("button", { name: "⚡ Run Match" }));

expect(
await screen.findByLabelText(/Allow facilities missing up to/),
).toBeInTheDocument();
expect(screen.getByText(/1 facility is hidden at this setting/)).toBeInTheDocument();
expect(screen.queryByText("No matches found")).not.toBeInTheDocument();

const slider = screen.getByLabelText(/Allow facilities missing up to/) as HTMLInputElement;
fireEvent.change(slider, { target: { value: "3" } });

expect(await screen.findByLabelText("Select Alpha Lab")).toBeInTheDocument();
});
});
173 changes: 92 additions & 81 deletions frontend/src/features/match/MatchView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -338,9 +338,10 @@ export function MatchView({
/>
)}

{view &&
{rawView &&
view &&
!mutation.isPending &&
(view.solutions.length === 0 ? (
(rawView.solutions.length === 0 ? (
<EmptyState
icon="🔍"
title="No matches found"
Expand Down Expand Up @@ -400,85 +401,95 @@ export function MatchView({
</div>
)}

<div className="flex flex-wrap items-center justify-between gap-3">
<p className="text-xs text-muted-foreground">
{view.totalSolutions} solution
{view.totalSolutions !== 1 ? "s" : ""}
{selectedSolutionKeys.length > 0
? ` · ${selectedSolutionKeys.length} selected`
: ""}
</p>
<div className="flex flex-wrap items-center gap-2">
<Button
variant="outline"
size="sm"
disabled={view.solutions.length === 0}
onClick={() =>
setSelectedSolutionKeys(
view.solutions.map((s, i) => solutionSelectionKey(s, i)),
)
}
>
Select all
</Button>
<Button
variant="outline"
size="sm"
disabled={selectedSolutionKeys.length === 0}
onClick={() => setSelectedSolutionKeys([])}
>
Clear selection
</Button>
<Button
size="sm"
disabled={
selectedSolutionKeys.length === 0 || !selectedDesign
}
onClick={() => {
const selectedSolutions = view.solutions.filter((s, i) =>
selectedSolutionKeys.includes(solutionSelectionKey(s, i)),
);
const state: RfqNavigationState = {
okhId: selectedDesign!.id,
okhTitle: formatOkhDisplayTitle(selectedDesign!.title),
okhFunction: selectedDesign!.function ?? undefined,
okhVersion: selectedDesign!.version ?? undefined,
solutions: toRfqSolutions(
selectedSolutions,
websiteByFacilityId,
),
};
navigate("/rfq", { state });
}}
>
Contact selected facilities →
</Button>
</div>
</div>
<p className="text-xs text-muted-foreground">
Select one or more facilities to generate outreach RFQs and
arrange production. Each card also links to that solution’s supply
tree when available.
</p>
{view.solutions.map((s, i) => {
const key = solutionSelectionKey(s, i);
return (
<MatchResultCard
key={key}
solution={s}
solutionId={view.solutionId}
selectionKey={key}
selected={selectedSolutionKeys.includes(key)}
onToggle={() =>
setSelectedSolutionKeys((prev) =>
prev.includes(key)
? prev.filter((k) => k !== key)
: [...prev, key],
)
}
/>
);
})}
{view.solutions.length === 0 ? (
<EmptyState
icon="🔍"
title="No matches within tolerance"
description="Every facility is missing more than the tolerance above allows. Increase it to see them."
/>
) : (
<>
<div className="flex flex-wrap items-center justify-between gap-3">
<p className="text-xs text-muted-foreground">
{view.totalSolutions} solution
{view.totalSolutions !== 1 ? "s" : ""}
{selectedSolutionKeys.length > 0
? ` · ${selectedSolutionKeys.length} selected`
: ""}
</p>
<div className="flex flex-wrap items-center gap-2">
<Button
variant="outline"
size="sm"
disabled={view.solutions.length === 0}
onClick={() =>
setSelectedSolutionKeys(
view.solutions.map((s, i) => solutionSelectionKey(s, i)),
)
}
>
Select all
</Button>
<Button
variant="outline"
size="sm"
disabled={selectedSolutionKeys.length === 0}
onClick={() => setSelectedSolutionKeys([])}
>
Clear selection
</Button>
<Button
size="sm"
disabled={
selectedSolutionKeys.length === 0 || !selectedDesign
}
onClick={() => {
const selectedSolutions = view.solutions.filter((s, i) =>
selectedSolutionKeys.includes(solutionSelectionKey(s, i)),
);
const state: RfqNavigationState = {
okhId: selectedDesign!.id,
okhTitle: formatOkhDisplayTitle(selectedDesign!.title),
okhFunction: selectedDesign!.function ?? undefined,
okhVersion: selectedDesign!.version ?? undefined,
solutions: toRfqSolutions(
selectedSolutions,
websiteByFacilityId,
),
};
navigate("/rfq", { state });
}}
>
Contact selected facilities →
</Button>
</div>
</div>
<p className="text-xs text-muted-foreground">
Select one or more facilities to generate outreach RFQs and
arrange production. Each card also links to that solution’s supply
tree when available.
</p>
{view.solutions.map((s, i) => {
const key = solutionSelectionKey(s, i);
return (
<MatchResultCard
key={key}
solution={s}
solutionId={view.solutionId}
selectionKey={key}
selected={selectedSolutionKeys.includes(key)}
onToggle={() =>
setSelectedSolutionKeys((prev) =>
prev.includes(key)
? prev.filter((k) => k !== key)
: [...prev, key],
)
}
/>
);
})}
</>
)}
</div>
))}
</div>
Expand Down
Loading