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
16 changes: 13 additions & 3 deletions frontend/src/explorer/ModelSelectionPage.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,19 @@ import { getApiUrl } from "src/envService";
const MODELS_URL = getApiUrl() + "/models";

const fakeModels = MockPayload.GET.getPayloadWithArrayOfFakeModelInfo(3);
fakeModels[0] = { ...fakeModels[0], name: "Taxonomy for South Africa", version: "v1.0.0.0" };
fakeModels[1] = { ...fakeModels[1], name: "Taxonomy for South Africa", version: "v1.0.1.1" };
fakeModels[2] = { ...fakeModels[2], name: "Tabiya esco-1.1.1", version: "v0.9.0" };
fakeModels[0] = {
...fakeModels[0],
name: "Taxonomy for South Africa",
version: "v1.0.0.0",
availableLanguages: ["en"],
};
fakeModels[1] = {
...fakeModels[1],
name: "Taxonomy for South Africa",
version: "v1.0.1.1",
availableLanguages: ["en", "fr", "es", "pt", "am"],
};
fakeModels[2] = { ...fakeModels[2], name: "Tabiya esco-1.1.1", version: "v0.9.0", availableLanguages: ["en"] };

const meta: Meta<typeof ModelSelectionPage> = {
title: "Explorer/ModelSelectionPage",
Expand Down
45 changes: 42 additions & 3 deletions frontend/src/explorer/ModelSelectionPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,29 @@
import "src/_test_utilities/consoleMock";

import { MemoryRouter, Route, Routes } from "react-router-dom";
import { render, screen } from "src/_test_utilities/test-utils";
import { render, screen, within } from "src/_test_utilities/test-utils";
import userEvent from "@testing-library/user-event";
import ModelSelectionPage, { DATA_TEST_ID } from "./ModelSelectionPage";
import ModelInfoService from "src/modelInfo/modelInfo.service";
import { MODELS_QUERY_KEY } from "src/modelInfo/useModels";
import { queryClient } from "src/app/providers/QueryProvider";
import { getArrayOfFakeModels } from "src/modeldirectory/_test_utilities/mockModelData";
import { routerPaths } from "src/app/routerPaths";
import { DATA_TEST_ID as AVAILABLE_LANGUAGES_DATA_TEST_ID } from "src/modeldirectory/components/AvailableLanguages/AvailableLanguages";

const givenModels = getArrayOfFakeModels(2);
givenModels[0] = { ...givenModels[0], name: "Taxonomy for South Africa", version: "v1.0.1-rc.1" };
givenModels[1] = { ...givenModels[1], name: "Tabiya esco-1.1.1", version: "v0.9.0" };
givenModels[0] = {
...givenModels[0],
name: "Taxonomy for South Africa",
version: "v1.0.1-rc.1",
availableLanguages: ["en"],
};
givenModels[1] = {
...givenModels[1],
name: "Tabiya esco-1.1.1",
version: "v0.9.0",
availableLanguages: ["en", "fr", "es", "pt", "am"],
};

const renderModelSelectionPage = () =>
render(
Expand Down Expand Up @@ -82,6 +93,34 @@ describe("ModelSelectionPage", () => {
expect(console.warn).not.toHaveBeenCalled();
});

test("should show the names of the available languages on each model card", async () => {
// GIVEN the models service resolves with a single language model and a model available in five languages
const [givenSingleLanguageModel, givenMultilingualModel] = givenModels;

// WHEN the page is rendered
renderModelSelectionPage();

// THEN expect the card of the multilingual model to show the names of its languages
const actualMultilingualLanguages = await screen.findByTestId(
`${DATA_TEST_ID.MODEL_CARD_AVAILABLE_LANGUAGES}-${givenMultilingualModel.id}`
);
expect(actualMultilingualLanguages).toHaveTextContent("English, French, Spanish, Portuguese, Amharic");
// AND the names to be inside the card of the multilingual model
expect(screen.getByTestId(`${DATA_TEST_ID.MODEL_CARD}-${givenMultilingualModel.id}`)).toContainElement(
actualMultilingualLanguages
);
// AND the card of the single language model to show the name of its language
const actualSingleLanguage = screen.getByTestId(
`${DATA_TEST_ID.MODEL_CARD_AVAILABLE_LANGUAGES}-${givenSingleLanguageModel.id}`
);
expect(
within(actualSingleLanguage).getByTestId(AVAILABLE_LANGUAGES_DATA_TEST_ID.AVAILABLE_LANGUAGES_NAMES)
).toHaveTextContent(/^English$/);
// AND expect no errors or warnings to have been logged
expect(console.error).not.toHaveBeenCalled();
expect(console.warn).not.toHaveBeenCalled();
});

test("should render the empty state without crashing when fetching the models fails", async () => {
// GIVEN fetching the models will fail, and retries are disabled
queryClient.setQueryDefaults(MODELS_QUERY_KEY, { retry: false });
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/explorer/ModelSelectionPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ import { ServiceError } from "src/error/error";
import { writeServiceErrorToLog } from "src/error/logger";
import ContentLayout from "src/theme/ContentLayout/ContentLayout";
import { routerPaths } from "src/app/routerPaths";
import AvailableLanguages from "src/modeldirectory/components/AvailableLanguages/AvailableLanguages";

const uniqueId = "3f7c1a5b-9e42-4d8f-b6c0-1a2e3f4d5c6b";

export const DATA_TEST_ID = {
MODEL_SELECTION_PAGE: `model-selection-page-${uniqueId}`,
MODEL_CARD: `model-selection-page-card-${uniqueId}`,
MODEL_CARD_AVAILABLE_LANGUAGES: `model-selection-page-card-available-languages-${uniqueId}`,
};

const ModelSelectionPage = () => {
Expand Down Expand Up @@ -86,6 +88,12 @@ const ModelSelectionPage = () => {
<Typography variant="body2" color="text.secondary" noWrap>
{m.locale?.name} ({m.locale?.shortCode})
</Typography>
<Box marginTop={theme.tabiyaSpacing.xs}>
<AvailableLanguages
availableLanguages={m.availableLanguages}
data-testid={`${DATA_TEST_ID.MODEL_CARD_AVAILABLE_LANGUAGES}-${m.id}`}
/>
</Box>
</Box>
{m.version && (
<Typography variant="body2" color="text.secondary" sx={{ fontFamily: "monospace", flexShrink: 0 }}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { Meta, StoryObj } from "@storybook/react";
import AvailableLanguages from "./AvailableLanguages";
import LanguageAPISpecs from "api-specifications/language";

const meta: Meta<typeof AvailableLanguages> = {
title: "ModelDirectory/AvailableLanguages",
component: AvailableLanguages,
tags: ["autodocs"],
};

export default meta;
type Story = StoryObj<typeof AvailableLanguages>;

export const SingleLanguage: Story = {
args: {
availableLanguages: ["en"],
},
};

export const MultipleLanguages: Story = {
args: {
availableLanguages: ["en", "am", "fr"],
},
};

export const WithLabel: Story = {
args: {
availableLanguages: ["en", "fr"],
showLabel: true,
},
};

export const WithLabelSingleLanguage: Story = {
args: {
availableLanguages: ["en"],
showLabel: true,
},
};

export const AllRegistryLanguages: Story = {
args: {
availableLanguages: LanguageAPISpecs.Constants.Languages.map((language) => language.shortCode),
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// mute the console
import "src/_test_utilities/consoleMock";

import { render, screen, within } from "src/_test_utilities/test-utils";
import AvailableLanguages, { DATA_TEST_ID, TEXT, getLanguageNames } from "./AvailableLanguages";

describe("AvailableLanguages", () => {
beforeEach(() => {
(console.error as jest.Mock).mockClear();
(console.warn as jest.Mock).mockClear();
});

test("should render the names of the available languages next to a language icon", () => {
// GIVEN the languages of a model available in three languages
const givenAvailableLanguages = ["en", "am", "fr"];

// WHEN the component is rendered
render(<AvailableLanguages availableLanguages={givenAvailableLanguages} />);

// THEN expect no errors or warning to have occurred
expect(console.error).not.toHaveBeenCalled();
expect(console.warn).not.toHaveBeenCalled();
// AND the names of the languages to be shown in the given order
const actualContainer = screen.getByTestId(DATA_TEST_ID.AVAILABLE_LANGUAGES);
expect(within(actualContainer).getByTestId(DATA_TEST_ID.AVAILABLE_LANGUAGES_NAMES)).toHaveTextContent(
/^English, Amharic, French$/
);
// AND no label to be shown
expect(within(actualContainer).queryByTestId(DATA_TEST_ID.AVAILABLE_LANGUAGES_LABEL)).not.toBeInTheDocument();
// AND the icon to tell screen readers what the names are
expect(within(actualContainer).getByRole("img", { name: "Languages" })).toHaveAttribute(
"data-testid",
DATA_TEST_ID.AVAILABLE_LANGUAGES_ICON
);
// AND to match the snapshot
expect(actualContainer).toMatchSnapshot();
});

test("should render the label before the names of the available languages when the label is shown", () => {
// GIVEN the languages of a model available in two languages
const givenAvailableLanguages = ["en", "fr"];
// AND the plural label expected for more than one language
const expectedLabel = "Languages:";

// WHEN the component is rendered with the label shown
render(<AvailableLanguages availableLanguages={givenAvailableLanguages} showLabel />);

// THEN expect no errors or warning to have occurred
expect(console.error).not.toHaveBeenCalled();
expect(console.warn).not.toHaveBeenCalled();
// AND the label to be shown
const actualContainer = screen.getByTestId(DATA_TEST_ID.AVAILABLE_LANGUAGES);
expect(within(actualContainer).getByTestId(DATA_TEST_ID.AVAILABLE_LANGUAGES_LABEL)).toHaveTextContent(
expectedLabel
);
// AND the names of the languages to be shown after the label
expect(actualContainer).toHaveTextContent(`${expectedLabel}English, French`);
// AND the icon to be hidden from screen readers, as the label already tells what the names are
expect(within(actualContainer).getByTestId(DATA_TEST_ID.AVAILABLE_LANGUAGES_ICON)).toHaveAttribute(
"aria-hidden",
"true"
);
// AND to match the snapshot
expect(actualContainer).toMatchSnapshot();
});

test("should use the singular for the label of a single available language", () => {
// GIVEN the language of a model available in a single language
const givenAvailableLanguages = ["en"];

// WHEN the component is rendered with the label shown
render(<AvailableLanguages availableLanguages={givenAvailableLanguages} showLabel />);

// THEN expect the label to be in the singular
expect(screen.getByTestId(DATA_TEST_ID.AVAILABLE_LANGUAGES_LABEL)).toHaveTextContent(/^Language:$/);
// AND the name of the language to be shown
expect(screen.getByTestId(DATA_TEST_ID.AVAILABLE_LANGUAGES_NAMES)).toHaveTextContent(/^English$/);
});

test("should use the singular for the icon title of a single available language", () => {
// GIVEN the language of a model available in a single language
const givenAvailableLanguages = ["en"];

// WHEN the component is rendered without the label
render(<AvailableLanguages availableLanguages={givenAvailableLanguages} />);

// THEN expect the icon to tell screen readers that the name is a language, in the singular
expect(screen.getByRole("img", { name: "Language" })).toHaveAttribute(
"data-testid",
DATA_TEST_ID.AVAILABLE_LANGUAGES_ICON
);
});

test("should render nothing when there are no available languages", () => {
// GIVEN no available languages
const givenAvailableLanguages: string[] = [];

// WHEN the component is rendered
const { container } = render(<AvailableLanguages availableLanguages={givenAvailableLanguages} />);

// THEN expect no errors or warning to have occurred
expect(console.error).not.toHaveBeenCalled();
expect(console.warn).not.toHaveBeenCalled();
// AND nothing to be rendered
expect(container).toBeEmptyDOMElement();
});

test("should use the given data-testid for the container", () => {
// GIVEN a custom data-testid
const givenTestId = "foo";

// WHEN the component is rendered with the given data-testid
render(<AvailableLanguages availableLanguages={["en", "fr"]} data-testid={givenTestId} />);

// THEN expect the container to have the given data-testid
expect(screen.getByTestId(givenTestId)).toHaveTextContent("English, French");
});
});

describe("TEXT.getLanguagesTitle", () => {
test.each([
[1, "Language"],
[2, "Languages"],
[5, "Languages"],
])("should return the title for %s language(s) as '%s'", (givenCount, expectedTitle) => {
// GIVEN a number of languages
// WHEN getting the title
const actualTitle = TEXT.getLanguagesTitle(givenCount);

// THEN expect the singular for one language and the plural otherwise
expect(actualTitle).toBe(expectedTitle);
});
});

describe("getLanguageNames", () => {
test("should return the registry names of the languages in the given order", () => {
// GIVEN the short codes of languages in the registry
const givenAvailableLanguages = ["fr", "en", "es"];

// WHEN resolving their names
const actualNames = getLanguageNames(givenAvailableLanguages);

// THEN expect the registry names, in the given order
expect(actualNames).toBe("French, English, Spanish");
});

test("should name a language that is not in the registry by its short code", () => {
// GIVEN a short code of a language that is not in the registry
const givenShortCode = "xx";

// WHEN resolving the names
const actualNames = getLanguageNames(["en", givenShortCode]);

// THEN expect the unknown language to be named by its short code
expect(actualNames).toBe(`English, ${givenShortCode}`);
});

test("should return an empty string when there are no languages", () => {
// GIVEN no languages
// WHEN resolving their names
const actualNames = getLanguageNames([]);

// THEN expect an empty string
expect(actualNames).toBe("");
});
});
Loading