Skip to content

Commit

Permalink
feat(machines): update DHCP and discovery forms to use the new API #1116
Browse files Browse the repository at this point in the history
  • Loading branch information
petermakowski authored Sep 20, 2022
1 parent 7aec6cd commit 409fc0b
Show file tree
Hide file tree
Showing 32 changed files with 701 additions and 113 deletions.
31 changes: 31 additions & 0 deletions cypress/e2e/with-users/settings/dhcp.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { generateMAASURL, generateName } from "../../utils";

context("Settings - DHCP Snippets", () => {
beforeEach(() => {
cy.login();
cy.addMachine();
cy.visit(generateMAASURL("/settings/dhcp/add"));
});

it("can add a DHCP snippet to a machine", () => {
const snippetName = generateName("dhcp-snippet");
cy.get("[data-testid='section-header-title']").contains("Settings");
cy.findByLabelText("Snippet name").type(snippetName);
cy.findByLabelText("Type").select("Machine");
cy.findByRole("button", { name: /Choose machine/ }).click();
// ensure the data has loaded
cy.findByRole("grid").should("have.attr", "aria-busy", "false");
cy.get("tbody").within(() => {
cy.findAllByRole("row").first().click();
});
cy.findByLabelText("DHCP snippet").type("ddns-update-style none;");
cy.findByRole("button", { name: "Save snippet" }).click();
// expect to be redirected to the list page
cy.findByLabelText("Search DHCP snippets").type(snippetName);
cy.findByRole("grid").within(() => {
cy.findByText(snippetName).should("be.visible");
cy.findByRole("button", { name: /Delete/ }).click();
cy.get("[data-testid='action-confirm']").click();
});
});
});
13 changes: 13 additions & 0 deletions cypress/support/commands.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import "@testing-library/cypress/add-commands";
import type { Result } from "axe-core";
import { nanoid } from "nanoid";
import { generateMAASURL, generateMac } from "../e2e/utils";
import type { A11yPageContext } from "./e2e";

Cypress.Commands.add("login", (options) => {
Expand Down Expand Up @@ -35,6 +37,17 @@ Cypress.Commands.add("loginNonAdmin", () => {
});
});

Cypress.Commands.add("addMachine", (hostname = `cypress-${nanoid()}`) => {
cy.visit(generateMAASURL("/machines"));
cy.get("[data-testid='add-hardware-dropdown'] button").click();
cy.get(".p-contextual-menu__link").contains("Machine").click();
cy.get("input[name='hostname']").type(hostname);
cy.get("input[name='pxe_mac']").type(generateMac());
cy.get("select[name='power_type']").select("manual").blur();
cy.get("button[type='submit']").click();
cy.get(`[data-testid='message']:contains(${hostname} added successfully.)`);
});

function logViolations(violations: Result[], pageContext: A11yPageContext) {
const divider =
"\n====================================================================================================\n";
Expand Down
1 change: 1 addition & 0 deletions cypress/support/e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type A11yPageContext = { url?: string; title?: string };
declare global {
namespace Cypress {
interface Chainable {
addMachine(hostname?: string): void;
login(options?: {
username?: string;
password?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ type Props = {
onDebounced: (debouncedText: string) => void;
searchText: string;
setSearchText: (searchText: string) => void;
} & Omit<SearchBoxProps, "externallyControlled" | "onChange" | "value">;
} & Omit<SearchBoxProps, "externallyControlled" | "onChange" | "value" | "ref">;

export const DEFAULT_DEBOUNCE_INTERVAL = 500;

Expand All @@ -22,6 +22,7 @@ const DebounceSearchBox = ({
onDebounced,
searchText,
setSearchText,
...props
}: Props): JSX.Element => {
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const [debouncing, setDebouncing] = useState(false);
Expand All @@ -38,6 +39,7 @@ const DebounceSearchBox = ({
return (
<div className="debounce-search-box">
<SearchBox
{...props}
externallyControlled
onChange={(text: string) => {
setDebouncing(true);
Expand Down
58 changes: 43 additions & 15 deletions src/app/base/components/DhcpFormFields/DhcpFormFields.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { render, screen } from "@testing-library/react";
import reduxToolkit from "@reduxjs/toolkit";
import { render, screen, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Provider } from "react-redux";
import { MemoryRouter } from "react-router-dom";
Expand All @@ -16,17 +17,20 @@ import {
dhcpSnippetState as dhcpSnippetStateFactory,
machine as machineFactory,
machineState as machineStateFactory,
machineStateList as machineStateListFactory,
machineStateListGroup as machineStateListGroupFactory,
subnet as subnetFactory,
subnetState as subnetStateFactory,
rootState as rootStateFactory,
} from "testing/factories";

const mockStore = configureStore();

const machines = [machineFactory()];
describe("DhcpFormFields", () => {
let state: RootState;

beforeEach(() => {
jest.spyOn(reduxToolkit, "nanoid").mockReturnValue("123456");
state = rootStateFactory({
controller: controllerStateFactory({ loaded: true }),
device: deviceStateFactory({ loaded: true }),
Expand All @@ -49,11 +53,19 @@ describe("DhcpFormFields", () => {
loaded: true,
}),
machine: machineStateFactory({
items: [
machineFactory({
fqdn: "node2.maas",
items: machines,
lists: {
"123456": machineStateListFactory({
loading: false,
loaded: true,
groups: [
machineStateListGroupFactory({
items: [machines[0].system_id],
name: "Deployed",
}),
],
}),
],
},
loaded: true,
}),
subnet: subnetStateFactory({
Expand Down Expand Up @@ -113,7 +125,7 @@ describe("DhcpFormFields", () => {
screen.getByRole("alert", { name: Labels.LoadingData })
).toBeInTheDocument();
expect(
screen.queryByRole("combobox", { name: Labels.Entity })
screen.queryByRole("combobox", { name: Labels.AppliesTo })
).not.toBeInTheDocument();
});

Expand All @@ -136,7 +148,7 @@ describe("DhcpFormFields", () => {
screen.queryByRole("alert", { name: Labels.LoadingData })
).not.toBeInTheDocument();
expect(
screen.getByRole("combobox", { name: Labels.Entity })
screen.getByRole("combobox", { name: Labels.AppliesTo })
).toBeInTheDocument();
});

Expand All @@ -154,15 +166,31 @@ describe("DhcpFormFields", () => {
);
// Set an initial type.
const typeSelect = screen.getByRole("combobox", { name: Labels.Type });
await userEvent.selectOptions(typeSelect, "machine");

await userEvent.selectOptions(typeSelect, "subnet");
await userEvent.selectOptions(
screen.getByRole("combobox", {
name: Labels.AppliesTo,
}),
"test.local"
);
// Select a machine. Value should get set.
const entitySelect = screen.getByRole("combobox", { name: Labels.Entity });
await userEvent.selectOptions(entitySelect, machine.system_id);
expect(entitySelect).toHaveValue(machine.system_id);

await userEvent.selectOptions(typeSelect, "machine");
await userEvent.click(
screen.getByRole("button", { name: /Choose machine/ })
);
await waitFor(() =>
expect(screen.getByRole("grid")).toHaveAttribute("aria-busy", "false")
);
within(screen.getByRole("grid")).getByText(machine.hostname).click();
expect(
screen.getByRole("button", { name: new RegExp(machine.hostname, "i") })
).toHaveAccessibleDescription(Labels.AppliesTo);
// Change the type. The select value should be cleared.
await userEvent.selectOptions(typeSelect, "subnet");
expect(entitySelect).toHaveValue("");
expect(
screen.getByRole("combobox", {
name: Labels.AppliesTo,
})
).toHaveValue("");
});
});
29 changes: 13 additions & 16 deletions src/app/base/components/DhcpFormFields/DhcpFormFields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
import { useFormikContext } from "formik";
import { useSelector } from "react-redux";

import MachineSelect from "./MachineSelect/MachineSelect";

import type { DHCPFormValues } from "app/base/components/DhcpForm/types";
import FormikField from "app/base/components/FormikField";
import controllerSelectors from "app/store/controller/selectors";
Expand Down Expand Up @@ -54,7 +56,7 @@ export enum Labels {
Description = "Description",
Disabled = "This snippet is disabled and will not be used by MAAS.",
Enabled = "Enabled",
Entity = "Applies to",
AppliesTo = "Applies to",
LoadingData = "Loading DHCP snippet data",
Name = "Snippet name",
Type = "Type",
Expand All @@ -73,12 +75,8 @@ export const DhcpFormFields = ({ editing }: Props): JSX.Element => {
const controllerLoaded = useSelector(controllerSelectors.loaded);
const deviceLoading = useSelector(deviceSelectors.loading);
const deviceLoaded = useSelector(deviceSelectors.loaded);
const machineLoading = useSelector(machineSelectors.loading);
const machineLoaded = useSelector(machineSelectors.loaded);
const isLoading =
subnetLoading || controllerLoading || deviceLoading || machineLoading;
const hasLoaded =
subnetLoaded && controllerLoaded && deviceLoaded && machineLoaded;
const isLoading = subnetLoading || controllerLoading || deviceLoading;
const hasLoaded = subnetLoaded && controllerLoaded && deviceLoaded;
const { enabled, type } = formikProps.values;
let models: ModelType[] | null;
switch (type) {
Expand All @@ -97,7 +95,6 @@ export const DhcpFormFields = ({ editing }: Props): JSX.Element => {
default:
models = null;
}

return (
<>
{editing && !enabled && (
Expand Down Expand Up @@ -134,21 +131,21 @@ export const DhcpFormFields = ({ editing }: Props): JSX.Element => {
{ value: "device", label: "Device" },
]}
/>
{type &&
{type === "machine" ? (
<FormikField component={MachineSelect} name="entity" />
) : (
type &&
(isLoading || !hasLoaded ? (
<Spinner aria-label={Labels.LoadingData} text="loading..." />
) : (
<FormikField
component={Select}
label={Labels.Entity}
label={Labels.AppliesTo}
name="entity"
options={
// This won't need to pass the empty array once this issue is fixed:
// https://github.com/canonical/react-components/issues/570
generateOptions(type, models) || []
}
options={generateOptions(type, models)}
/>
))}
))
)}
<FormikField
component={Textarea}
grow
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Formik } from "formik";

import MachineSelect, { Labels } from "./MachineSelect";

import { renderWithMockStore } from "testing/utils";

it("can open select box on click", async () => {
renderWithMockStore(
<Formik initialValues={{ machine: "" }} onSubmit={jest.fn()}>
<MachineSelect name="machine" />
</Formik>
);

expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
await userEvent.click(
screen.getByRole("button", { name: new RegExp(Labels.ChooseMachine, "i") })
);
expect(screen.getByRole("listbox")).toBeInTheDocument();
});

it("sets focus on the input field on open", async () => {
renderWithMockStore(
<Formik initialValues={{ machine: "" }} onSubmit={jest.fn()}>
<MachineSelect name="machine" />
</Formik>
);

await userEvent.click(
screen.getByRole("button", { name: new RegExp(Labels.ChooseMachine, "i") })
);
expect(
screen.getByPlaceholderText("Search by hostname, system ID or tags")
).toHaveFocus();
});
Loading

0 comments on commit 409fc0b

Please sign in to comment.