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
4 changes: 2 additions & 2 deletions frontend/src/app/__tests__/cattle-info.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1218,7 +1218,7 @@ describe("Cattle Info — state-driven fields (GET /v1/animal/cattle-info-fields
fillSimulationName();
await selectCountry("India");

await pickCategory("Baby calves/heifers");
await pickCategory("Calves");
await waitFor(() => expect(inputAfterLabel("Body Weight (BW; kg)").value).toBe("40"));
for (const hidden of [
"Breed Selection",
Expand Down Expand Up @@ -1285,7 +1285,7 @@ describe("Cattle Info — state-driven fields (GET /v1/animal/cattle-info-fields
await formReady();
fillSimulationName();
await selectCountry("India");
await pickCategory("Baby calves/heifers");
await pickCategory("Calves");
await waitFor(() => expect(inputAfterLabel("Body Weight (BW; kg)").value).toBe("40"));

fireEvent.click(screen.getByRole("button", { name: "Continue" }));
Expand Down
60 changes: 60 additions & 0 deletions frontend/src/components/__tests__/useCustomFeed.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { renderHook, act } from "@testing-library/react";

const { getCatalogCategoriesForType, getFeedCategories, getFeedTypes } = vi.hoisted(() => ({
getCatalogCategoriesForType: vi.fn(),
getFeedCategories: vi.fn(),
getFeedTypes: vi.fn(),
}));

vi.mock("@/lib/api", async () => {
const actual = await vi.importActual<typeof import("@/lib/api")>("@/lib/api");
return { ...actual, getCatalogCategoriesForType, getFeedCategories, getFeedTypes };
});

import { useCustomFeed } from "@/components/feed-selection/useCustomFeed";
import type { User } from "@/lib/store";

const user = { id: "u-1", name: "A", country: "India", country_id: "1" } as User;
const MASTER = [
"By-Product/Other",
"Plant Protein",
"Animal Protein",
"Energy Source",
"Minerals",
"Additive",
"Concentrate Mixtures",
];

async function categoriesFor(feedType: string) {
const showSnackbar = vi.fn();
const { result } = renderHook(() => useCustomFeed(user, (s) => s, showSnackbar));
await act(async () => {
await result.current.sheetProps.handleCustomTypeChange(feedType);
});
return { categories: result.current.sheetProps.customFeedCategories, showSnackbar };
}

beforeEach(() => {
vi.resetAllMocks();
// The country (India) only has feeds in Energy Source.
getFeedCategories.mockResolvedValue({ data: ["Energy Source"] });
});

describe("Custom Feed categories (N6)", () => {
it("lists every master category of the type, in catalogue order, not just the country's", async () => {
getCatalogCategoriesForType.mockResolvedValue(MASTER);
const { categories } = await categoriesFor("Concentrate");
expect(getCatalogCategoriesForType).toHaveBeenCalledWith("Concentrate");
expect(categories).toEqual(MASTER);
expect(getFeedCategories).not.toHaveBeenCalled();
});

it("falls back to the country's categories when the master lookup fails", async () => {
getCatalogCategoriesForType.mockResolvedValue(null);
const { categories, showSnackbar } = await categoriesFor("Concentrate");
expect(getFeedCategories).toHaveBeenCalledWith("Concentrate", "1", "u-1");
expect(categories).toEqual(["Energy Source"]);
expect(showSnackbar).not.toHaveBeenCalled();
});
});
14 changes: 11 additions & 3 deletions frontend/src/components/feed-selection/useCustomFeed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { useState } from "react";
import {
checkInsertOrUpdate,
getCatalogCategoriesForType,
getFeedCategories,
getFeedTypes,
insertCustomFeed,
Expand Down Expand Up @@ -62,9 +63,16 @@ export function useCustomFeed(user: User | null, t: (s: string) => string, showS
if (!feedType || !user?.country_id || !user?.id) return;
setLoadingCustomCats(true);
try {
const res = await getFeedCategories(feedType, user.country_id, user.id);
const names = parseFeedCategoryOptions(res.data).map((o) => o.name);
setCustomFeedCategories(names);
// N6: a new feed may go in ANY category of its type, not only the ones
// this country already has feeds in — so the master list, in its own
// order. If that lookup fails, the country's list keeps it usable.
const catalog = await getCatalogCategoriesForType(feedType);
if (catalog) {
setCustomFeedCategories(catalog);
} else {
const res = await getFeedCategories(feedType, user.country_id, user.id);
setCustomFeedCategories(parseFeedCategoryOptions(res.data).map((o) => o.name));
}
} catch {
showSnackbar(t("Could not load categories"), "error");
} finally {
Expand Down
15 changes: 15 additions & 0 deletions frontend/src/lib/__tests__/api-feed-taxonomy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
getFeedTypesLocalized,
getFeedCategories,
getFeedCategoriesLocalized,
getCatalogCategoriesForType,
getFeedSubCategories,
fetchFeedTaxonomyLabels,
evaluateDiet,
Expand Down Expand Up @@ -193,6 +194,20 @@ describe("getFeedCategories", () => {
const res = await getFeedCategories("Complete diet", "7");
expect(res.data).toEqual({ feed_categories: ["Complete diet"] });
});

// N6: Custom Feed creates a feed, so it offers EVERY master category of
// the type in catalogue order; Feed Selection above keeps the overlap.
it("getCatalogCategoriesForType gives every master category, not the country overlap", async () => {
const all = ["By-Product/Other", "Plant Protein", "Energy Source", "Minerals", "Additive"];
routeGet(["Energy Source"], { "type-conc": all.map((category_name) => ({ category_name })) });
expect(await getCatalogCategoriesForType("Concentrate")).toEqual(all);
expect((await getFeedCategories("Concentrate", "7")).data).toEqual(["Energy Source"]);
});

it("getCatalogCategoriesForType returns null when the master list can't be read", async () => {
routeGet(["Energy Source"], {}, { typesFail: true });
expect(await getCatalogCategoriesForType("Concentrate")).toBeNull();
});
});

describe("getFeedSubCategories", () => {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/lib/__tests__/api-helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ describe("animal category helpers", () => {
expect(ANIMAL_CATEGORY_LABELS["Lactating Cow"]).toBe("Lactating cows");
expect(ANIMAL_CATEGORY_LABELS["Dry Cow"]).toBe("Dry cows");
expect(ANIMAL_CATEGORY_LABELS["Heifer"]).toBe("Heifers");
expect(ANIMAL_CATEGORY_LABELS["Baby Calf/Heifer"]).toBe("Baby calves/heifers");
expect(ANIMAL_CATEGORY_LABELS["Baby Calf/Heifer"]).toBe("Calves");
});

it("isLactating is true only for Lactating Cow", () => {
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/lib/api-client/feeds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,15 @@ async function catalogCategoryNamesForType(feed_type: string): Promise<string[]
}
}

/**
* Every active category of a feed type, from the master list, in catalogue
* order — whether or not the country has feeds in it yet. For creating a
* feed (Custom Feed, N6); Feed Selection keeps getFeedCategories, which
* offers only categories with feeds. null when the lookup fails.
*/
export const getCatalogCategoriesForType = (feed_type: string): Promise<string[] | null> =>
catalogCategoryNamesForType(feed_type);

async function allowedCategoryNamesForType(feed_type: string): Promise<Set<string> | null> {
const names = await catalogCategoryNamesForType(feed_type);
return names ? new Set(names.map(normalizeTaxonomyName)) : null;
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/lib/api-client/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export const ANIMAL_CATEGORY_LABELS: Record<AnimalCategory, string> = {
"Lactating Cow": "Lactating cows",
"Dry Cow": "Dry cows",
Heifer: "Heifers",
"Baby Calf/Heifer": "Baby calves/heifers",
"Baby Calf/Heifer": "Calves",
};
export const isLactating = (cat: AnimalCategory): boolean => cat === "Lactating Cow";

Expand Down
1 change: 1 addition & 0 deletions frontend/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export {
checkInsertOrUpdate,
fetchFeedTaxonomyLabels,
getFeedCategories,
getCatalogCategoriesForType,
getFeedCategoriesLocalized,
getFeedClassification,
getFeedDetails,
Expand Down
Binary file modified frontend/src/lib/data/ui-labels.xlsx
Binary file not shown.
Loading