Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
5 changes: 5 additions & 0 deletions .changeset/light-corners-lick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"landscape-ui": patch
---

Allow selection of more than one architecture when creating a publication
Comment on lines +1 to +5
5 changes: 5 additions & 0 deletions .changeset/twelve-suns-sing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"landscape-ui": patch
---

Keyboard a11y and arrow-key navigation for saved-searches dropdown
Comment on lines +1 to +5
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { renderWithProviders } from "@/tests/render";
import { describe, expect } from "vitest";
import { screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import MirrorDetails from "./MirrorDetails";
import { mirrors } from "@/tests/mocks/mirrors";
import { Suspense } from "react";
import LoadingState from "@/components/layout/LoadingState";
import { expectLoadingState } from "@/tests/helpers";
import { screen } from "@testing-library/react";

describe("MirrorDetails", () => {
it("renders", async () => {
it("renders the mirror display name once loaded", async () => {
renderWithProviders(
<Suspense fallback={<LoadingState />}>
<MirrorDetails />
Expand All @@ -17,7 +17,31 @@ describe("MirrorDetails", () => {
`?name=${mirrors[0].name}`,
);

expect(
await screen.findByRole("heading", { name: mirrors[0].displayName }),
).toBeInTheDocument();
});

it("displays preserve signatures status", async () => {
Comment thread
marqode marked this conversation as resolved.
Outdated
const mirrorWithPreserveSignatures = mirrors.find(
({ preserveSignatures }) => preserveSignatures,
);

assert(mirrorWithPreserveSignatures);

renderWithProviders(
<Suspense fallback={<LoadingState />}>
<MirrorDetails />
</Suspense>,
undefined,
`?name=${mirrorWithPreserveSignatures.name}`,
);

await expectLoadingState();

const label = screen.getByText("Preserve signatures");
Comment thread
marqode marked this conversation as resolved.
Outdated
expect(label).toBeInTheDocument();
expect(label.closest("div")?.nextSibling?.textContent).toBe("Yes");
});

it("displays preserve signatures status", async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import LoadingState from "@/components/layout/LoadingState";
import { renderWithProviders } from "@/tests/render";
import { screen, waitFor } from "@testing-library/react";
import { screen, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Suspense } from "react";
import { describe, expect, it } from "vitest";
Expand Down Expand Up @@ -60,8 +60,6 @@ describe("AddPublicationForm", () => {
expect(
screen.getByRole("combobox", { name: "Architectures" }),
).toBeInTheDocument();
expect(screen.getByRole("option", { name: "amd64" })).toBeInTheDocument();
expect(screen.getByRole("option", { name: "arm64" })).toBeInTheDocument();
});

it("updates mirror publication fields based on the selected source", async () => {
Expand All @@ -87,10 +85,6 @@ describe("AddPublicationForm", () => {
publicationTargetSelect,
"aaaaaaaa-0000-0000-0000-000000000001",
);
await user.selectOptions(
screen.getByRole("combobox", { name: "Architectures" }),
"amd64",
);
await user.type(
screen.getByRole("textbox", { name: "Directory prefix" }),
"edge",
Expand All @@ -108,13 +102,19 @@ describe("AddPublicationForm", () => {
await user.click(
screen.getByRole("checkbox", { name: /Skip content indexing/i }),
);
const archCombobox = screen.getByRole("combobox", {
name: "Architectures",
});
await user.click(archCombobox);
await user.click(await screen.findByRole("checkbox", { name: "amd64" }));

expect(publicationTargetSelect).toHaveValue(
"aaaaaaaa-0000-0000-0000-000000000001",
);
expect(screen.getByRole("combobox", { name: "Architectures" })).toHaveValue(
"amd64",
);
expect(screen.getByRole("checkbox", { name: "amd64" })).toBeChecked();
expect(
screen.getByRole("combobox", { name: "Architectures" }),
).toBeInTheDocument();
expect(
screen.getByRole("textbox", { name: "Directory prefix" }),
).toHaveValue("edge");
Expand Down Expand Up @@ -189,4 +189,161 @@ describe("AddPublicationForm", () => {
screen.queryByRole("heading", { name: "Signing GPG Key" }),
).not.toBeInTheDocument();
});

it("shows validation error when all architectures are deselected", async () => {
const user = userEvent.setup();

renderForm();

await selectMirrorSource(user);

const archCombobox = screen.getByRole("combobox", {
name: "Architectures",
});
await user.click(archCombobox);

const amd64Checkbox = await screen.findByRole("checkbox", {
name: "amd64",
});
await user.click(amd64Checkbox);
await user.click(amd64Checkbox);

await waitFor(() => {
expect(screen.getAllByText("This field is required")).not.toHaveLength(0);
});
});

it("shows success notification after submitting a valid local source publication", async () => {
const user = userEvent.setup();

renderForm();

await user.type(
await screen.findByRole("textbox", { name: "Publication name" }),
"new-local-publication",
);
await selectLocalSource(user);

const publicationTargetSelect = screen.getByRole("combobox", {
name: "Publication target",
});
await waitFor(() => {
expect(publicationTargetSelect).toBeEnabled();
});
await user.selectOptions(
publicationTargetSelect,
"bbbbbbbb-0000-0000-0000-000000000002",
);

await user.click(screen.getByRole("button", { name: "Add publication" }));

expect(
await screen.findByText(
'Publication "new-local-publication" has been created.',
),
).toBeInTheDocument();
});

it("shows success notification after submitting a mirror publication with a custom signing key", async () => {
const user = userEvent.setup();

renderForm();

await user.type(
await screen.findByRole("textbox", { name: "Publication name" }),
"new-mirror-publication",
);
await selectMirrorSource(user);

const publicationTargetSelect = screen.getByRole("combobox", {
name: "Publication target",
});
await waitFor(() => {
expect(publicationTargetSelect).toBeEnabled();
});
await user.selectOptions(
publicationTargetSelect,
"aaaaaaaa-0000-0000-0000-000000000001",
);

const signingKeySection = screen
.getByRole("heading", { name: "Signing GPG Key" })
.closest("section");
if (!signingKeySection)
throw new Error("Signing GPG Key section not found");
await user.type(
within(signingKeySection).getByRole("textbox"),
"-----BEGIN PGP PRIVATE KEY BLOCK-----test-key",
);

const archCombobox = screen.getByRole("combobox", {
name: "Architectures",
});
await user.click(archCombobox);
await user.click(await screen.findByRole("checkbox", { name: "amd64" }));

await user.click(screen.getByRole("button", { name: "Add publication" }));

expect(
await screen.findByText(
'Publication "new-mirror-publication" has been created.',
),
).toBeInTheDocument();
});

it("allows selecting multiple architectures simultaneously", async () => {
const user = userEvent.setup();

renderForm();

await selectMirrorSource(user);

const archCombobox = screen.getByRole("combobox", {
name: "Architectures",
});
await user.click(archCombobox);

await user.click(await screen.findByRole("checkbox", { name: "amd64" }));
await user.click(screen.getByRole("checkbox", { name: "arm64" }));

expect(screen.getByRole("checkbox", { name: "amd64" })).toBeChecked();
expect(screen.getByRole("checkbox", { name: "arm64" })).toBeChecked();
});

it("shows success notification after submitting a valid publication", async () => {
const user = userEvent.setup();

renderForm();

await user.type(
await screen.findByRole("textbox", { name: "Publication name" }),
"new-mirror-publication",
);
await selectMirrorSource(user);

const publicationTargetSelect = screen.getByRole("combobox", {
name: "Publication target",
});
await waitFor(() => {
expect(publicationTargetSelect).toBeEnabled();
});
await user.selectOptions(
publicationTargetSelect,
"aaaaaaaa-0000-0000-0000-000000000001",
);

const archCombobox = screen.getByRole("combobox", {
name: "Architectures",
});
await user.click(archCombobox);
await user.click(await screen.findByRole("checkbox", { name: "amd64" }));

await user.click(screen.getByRole("button", { name: "Add publication" }));

expect(
await screen.findByText(
'Publication "new-mirror-publication" has been created.',
),
).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import SidePanelFormButtons from "@/components/form/SidePanelFormButtons";
import ReadOnlyField from "@/components/form/ReadOnlyField";
import MultiSelectField from "@/components/form/MultiSelectField";
import Blocks from "@/components/layout/Blocks";
import { useGetLocalRepositories } from "@/features/local-repositories";
import { useListMirrors } from "@/features/mirrors";
Expand All @@ -16,6 +17,7 @@ import {
Select,
Textarea,
Tooltip,
type MultiSelectItem,
} from "@canonical/react-components";
import { useFormik } from "formik";
import type { FC } from "react";
Expand Down Expand Up @@ -139,17 +141,12 @@ const AddPublicationForm: FC = () => {
[publicationTargets],
);

const architectureOptions = useMemo(
() => [
{
label: "Select architecture",
value: "",
},
...(selectedSource?.architectures ?? []).map((architecture) => ({
const architectureItems = useMemo(
() =>
(selectedSource?.architectures ?? []).map((architecture) => ({
label: architecture,
value: architecture,
})),
],
[selectedSource],
);

Expand All @@ -158,8 +155,7 @@ const AddPublicationForm: FC = () => {
): Promise<void> => {
await formik.setFieldValue("source_type", event.target.value);
await formik.setFieldValue("source", "");
Comment thread
marqode marked this conversation as resolved.
Comment thread
marqode marked this conversation as resolved.
await formik.setFieldValue("uploader_distribution", "");
await formik.setFieldValue("uploader_architectures", "");
await formik.setFieldValue("uploader_architectures", []);
await formik.setFieldValue("signing_key", "");
};

Expand All @@ -175,15 +171,17 @@ const AddPublicationForm: FC = () => {
source?.distribution ?? "",
);

if (source?.sourceType === SOURCE_TYPE_LOCAL_REPOSITORY) {
await formik.setFieldValue("uploader_architectures", "");
await formik.setFieldValue("signing_key", "");

return;
}
await formik.setFieldValue("uploader_architectures", []);
Comment thread
marqode marked this conversation as resolved.
Comment thread
marqode marked this conversation as resolved.
};

await formik.setFieldValue("uploader_architectures", "");
await formik.setFieldValue("signing_key", "");
const handleArchitectureChange = async (
items: MultiSelectItem[],
): Promise<void> => {
await formik.setFieldTouched("uploader_architectures", true);
await formik.setFieldValue(
"uploader_architectures",
items.map(({ value }) => String(value)),
);
};

return (
Expand Down Expand Up @@ -243,13 +241,18 @@ const AddPublicationForm: FC = () => {
/>

{!isLocalSourceType && (
<Select
<MultiSelectField
variant="condensed"
hasSelectedItemsFirst={false}
label="Architectures"
required
disabled={!formik.values.source}
options={architectureOptions}
items={architectureItems}
selectedItems={architectureItems.filter(({ value }) =>
formik.values.uploader_architectures.includes(value),
)}
onItemsUpdate={handleArchitectureChange}
error={getFormikError(formik, "uploader_architectures")}
{...formik.getFieldProps("uploader_architectures")}
/>
)}
</Blocks.Item>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ export const INITIAL_VALUES: FormProps = {
publication_target: "",
prefix: "",
uploader_distribution: "",
uploader_architectures: "",
signing_key: "",
uploader_architectures: [],
hash_indexing: false,
automatic_installation: false,
automatic_upgrades: false,
Expand Down
Loading