diff --git a/packages/manager/.changeset/pr-10945-tests-1726507532580.md b/packages/manager/.changeset/pr-10945-tests-1726507532580.md
new file mode 100644
index 00000000000..f3b7edd947e
--- /dev/null
+++ b/packages/manager/.changeset/pr-10945-tests-1726507532580.md
@@ -0,0 +1,5 @@
+---
+"@linode/manager": Tests
+---
+
+Add unit tests for rest of NodeBalancers package ([#10945](https://github.com/linode/manager/pull/10945))
diff --git a/packages/manager/src/features/NodeBalancers/ConfigNodeIPSelect.tsx b/packages/manager/src/features/NodeBalancers/ConfigNodeIPSelect.tsx
index 1c971babe18..6630b7509c9 100644
--- a/packages/manager/src/features/NodeBalancers/ConfigNodeIPSelect.tsx
+++ b/packages/manager/src/features/NodeBalancers/ConfigNodeIPSelect.tsx
@@ -1,4 +1,3 @@
-import { Linode } from '@linode/api-v4/lib/linodes';
import { Box } from '@mui/material';
import * as React from 'react';
@@ -6,6 +5,7 @@ import { SelectedIcon } from 'src/components/Autocomplete/Autocomplete.styles';
import { LinodeSelect } from 'src/features/Linodes/LinodeSelect/LinodeSelect';
import { privateIPRegex } from 'src/utilities/ipUtils';
+import type { Linode } from '@linode/api-v4/lib/linodes';
import type { TextFieldProps } from 'src/components/TextField';
interface ConfigNodeIPSelectProps {
diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerConfigNode.test.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerConfigNode.test.tsx
new file mode 100644
index 00000000000..6bbd8e2248f
--- /dev/null
+++ b/packages/manager/src/features/NodeBalancers/NodeBalancerConfigNode.test.tsx
@@ -0,0 +1,76 @@
+import { fireEvent } from '@testing-library/react';
+import * as React from 'react';
+
+import { renderWithTheme } from 'src/utilities/testHelpers';
+
+import { NodeBalancerConfigNode } from './NodeBalancerConfigNode';
+
+import type { NodeBalancerConfigNodeProps } from './NodeBalancerConfigNode';
+
+const node = {
+ address: 'some address',
+ label: 'some label',
+};
+
+const props: NodeBalancerConfigNodeProps = {
+ configIdx: 1,
+ disabled: false,
+ forEdit: true,
+ idx: 1,
+ node,
+ onNodeAddressChange: vi.fn(),
+ onNodeLabelChange: vi.fn(),
+ onNodeModeChange: vi.fn(),
+ onNodePortChange: vi.fn(),
+ onNodeWeightChange: vi.fn(),
+ removeNode: vi.fn(),
+};
+
+describe('NodeBalancerConfigNode', () => {
+ it('renders the NodeBalancerConfigNode', () => {
+ const { getByLabelText, getByText, queryByText } = renderWithTheme(
+
+ );
+
+ expect(getByLabelText('Label')).toBeVisible();
+ expect(getByLabelText('Port')).toBeVisible();
+ expect(getByLabelText('Weight')).toBeVisible();
+ expect(getByText('Mode')).toBeVisible();
+ expect(getByText('Remove')).toBeVisible();
+ expect(queryByText('Status')).not.toBeInTheDocument();
+ });
+
+ it('renders the node status', () => {
+ const { getByText } = renderWithTheme(
+
+ );
+
+ expect(getByText('Status')).toBeVisible();
+ expect(getByText('DOWN')).toBeVisible();
+ });
+
+ it('cannot change the mode if the node is not for edit', () => {
+ const { queryByText } = renderWithTheme(
+
+ );
+
+ expect(queryByText('Mode')).not.toBeInTheDocument();
+ });
+
+ it('cannot remove the node if the node is not for edit or is the first node', () => {
+ const { queryByText } = renderWithTheme(
+
+ );
+
+ expect(queryByText('Remove')).not.toBeInTheDocument();
+ });
+
+ it('removes the node', () => {
+ const { getByText } = renderWithTheme(
+
+ );
+
+ fireEvent.click(getByText('Remove'));
+ expect(props.removeNode).toHaveBeenCalled();
+ });
+});
diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerConfigNode.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerConfigNode.tsx
index af6e03a1118..05f47659c98 100644
--- a/packages/manager/src/features/NodeBalancers/NodeBalancerConfigNode.tsx
+++ b/packages/manager/src/features/NodeBalancers/NodeBalancerConfigNode.tsx
@@ -1,5 +1,5 @@
-import Grid from '@mui/material/Unstable_Grid2';
import { styled } from '@mui/material/styles';
+import Grid from '@mui/material/Unstable_Grid2';
import * as React from 'react';
import { Autocomplete } from 'src/components/Autocomplete/Autocomplete';
@@ -13,8 +13,8 @@ import { Typography } from 'src/components/Typography';
import { getErrorMap } from 'src/utilities/errorUtils';
import { ConfigNodeIPSelect } from './ConfigNodeIPSelect';
-import { NodeBalancerConfigNodeFields } from './types';
+import type { NodeBalancerConfigNodeFields } from './types';
import type { NodeBalancerConfigNodeMode } from '@linode/api-v4';
export interface NodeBalancerConfigNodeProps {
diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerConfigPanel.test.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerConfigPanel.test.tsx
index 7f2716c6f78..13c7bdfcd36 100644
--- a/packages/manager/src/features/NodeBalancers/NodeBalancerConfigPanel.test.tsx
+++ b/packages/manager/src/features/NodeBalancers/NodeBalancerConfigPanel.test.tsx
@@ -28,7 +28,7 @@ const node: NodeBalancerConfigNodeFields = {
weight: 100,
};
-const props: NodeBalancerConfigPanelProps = {
+export const nbConfigPanelMockPropsForTest: NodeBalancerConfigPanelProps = {
addNode: vi.fn(),
algorithm: 'roundrobin',
checkBody: '',
@@ -70,7 +70,17 @@ const props: NodeBalancerConfigPanelProps = {
sslCertificate: '',
};
-const activeHealthChecks = ['Interval', 'Timeout', 'Attempts'];
+const activeHealthChecksFormInputs = ['Interval', 'Timeout', 'Attempts'];
+
+const activeHealthChecksHelperText = [
+ 'Seconds between health check probes',
+ 'Seconds to wait before considering the probe a failure. 1-30. Must be less than check_interval.',
+ 'Number of failed probes before taking a node out of rotation. 1-30',
+];
+
+const sslCertificate = 'ssl-certificate';
+const privateKey = 'private-key';
+const proxyProtocol = 'Proxy Protocol';
describe('NodeBalancerConfigPanel', () => {
it('renders the NodeBalancerConfigPanel', () => {
@@ -79,7 +89,10 @@ describe('NodeBalancerConfigPanel', () => {
getByText,
queryByLabelText,
queryByTestId,
- } = renderWithTheme();
+ queryByText,
+ } = renderWithTheme(
+
+ );
expect(getByLabelText('Protocol')).toBeVisible();
expect(getByLabelText('Algorithm')).toBeVisible();
@@ -109,67 +122,99 @@ describe('NodeBalancerConfigPanel', () => {
expect(getByText('Add a Node')).toBeVisible();
expect(getByText('Backend Nodes')).toBeVisible();
- activeHealthChecks.forEach((type) => {
- expect(queryByLabelText(type)).not.toBeInTheDocument();
+ activeHealthChecksFormInputs.forEach((formLabel) => {
+ expect(queryByLabelText(formLabel)).not.toBeInTheDocument();
});
- expect(queryByTestId('ssl-certificate')).not.toBeInTheDocument();
- expect(queryByTestId('private-key')).not.toBeInTheDocument();
+ activeHealthChecksHelperText.forEach((helperText) => {
+ expect(queryByText(helperText)).not.toBeInTheDocument();
+ });
+ expect(queryByTestId(sslCertificate)).not.toBeInTheDocument();
+ expect(queryByTestId(privateKey)).not.toBeInTheDocument();
expect(queryByTestId('http-path')).not.toBeInTheDocument();
expect(queryByTestId('http-body')).not.toBeInTheDocument();
- expect(queryByLabelText('Proxy Protocol')).not.toBeInTheDocument();
+ expect(queryByLabelText(proxyProtocol)).not.toBeInTheDocument();
});
it('renders form fields specific to the HTTPS protocol', () => {
const { getByTestId, queryByLabelText } = renderWithTheme(
-
+
);
- expect(getByTestId('ssl-certificate')).toBeVisible();
- expect(getByTestId('private-key')).toBeVisible();
- expect(queryByLabelText('Proxy Protocol')).not.toBeInTheDocument();
+ expect(getByTestId(sslCertificate)).toBeVisible();
+ expect(getByTestId(privateKey)).toBeVisible();
+ expect(queryByLabelText(proxyProtocol)).not.toBeInTheDocument();
});
it('renders form fields specific to the TCP protocol', () => {
const { getByLabelText, queryByTestId } = renderWithTheme(
-
+
);
- expect(getByLabelText('Proxy Protocol')).toBeVisible();
- expect(queryByTestId('ssl-certificate')).not.toBeInTheDocument();
- expect(queryByTestId('private-key')).not.toBeInTheDocument();
+ expect(getByLabelText(proxyProtocol)).toBeVisible();
+ expect(queryByTestId(sslCertificate)).not.toBeInTheDocument();
+ expect(queryByTestId(privateKey)).not.toBeInTheDocument();
});
it('renders fields specific to the Active Health Check type of TCP Connection', () => {
- const { getByLabelText, queryByTestId } = renderWithTheme(
-
+ const { getByLabelText, getByText, queryByTestId } = renderWithTheme(
+
);
- activeHealthChecks.forEach((type) => {
- expect(getByLabelText(type)).toBeVisible();
+ activeHealthChecksFormInputs.forEach((formLabel) => {
+ expect(getByLabelText(formLabel)).toBeVisible();
+ });
+ activeHealthChecksHelperText.forEach((helperText) => {
+ expect(getByText(helperText)).toBeVisible();
});
expect(queryByTestId('http-path')).not.toBeInTheDocument();
expect(queryByTestId('http-body')).not.toBeInTheDocument();
});
it('renders fields specific to the Active Health Check type of HTTP Status', () => {
- const { getByLabelText, getByTestId, queryByTestId } = renderWithTheme(
-
+ const {
+ getByLabelText,
+ getByTestId,
+ getByText,
+ queryByTestId,
+ } = renderWithTheme(
+
);
- activeHealthChecks.forEach((type) => {
- expect(getByLabelText(type)).toBeVisible();
+ activeHealthChecksFormInputs.forEach((formLabel) => {
+ expect(getByLabelText(formLabel)).toBeVisible();
+ });
+ activeHealthChecksHelperText.forEach((helperText) => {
+ expect(getByText(helperText)).toBeVisible();
});
expect(getByTestId('http-path')).toBeVisible();
expect(queryByTestId('http-body')).not.toBeInTheDocument();
});
it('renders fields specific to the Active Health Check type of HTTP Body', () => {
- const { getByLabelText, getByTestId } = renderWithTheme(
-
+ const { getByLabelText, getByTestId, getByText } = renderWithTheme(
+
);
- activeHealthChecks.forEach((type) => {
- expect(getByLabelText(type)).toBeVisible();
+ activeHealthChecksFormInputs.forEach((formLabel) => {
+ expect(getByLabelText(formLabel)).toBeVisible();
+ });
+ activeHealthChecksHelperText.forEach((helperText) => {
+ expect(getByText(helperText)).toBeVisible();
});
expect(getByTestId('http-path')).toBeVisible();
expect(getByTestId('http-body')).toBeVisible();
@@ -177,7 +222,7 @@ describe('NodeBalancerConfigPanel', () => {
it('renders the relevant helper text for the Round Robin algorithm', () => {
const { getByText, queryByText } = renderWithTheme(
-
+
);
expect(getByText(ROUND_ROBIN_ALGORITHM_HELPER_TEXT)).toBeVisible();
@@ -189,7 +234,10 @@ describe('NodeBalancerConfigPanel', () => {
it('renders the relevant helper text for the Least Connections algorithm', () => {
const { getByText, queryByText } = renderWithTheme(
-
+
);
expect(getByText(LEAST_CONNECTIONS_ALGORITHM_HELPER_TEXT)).toBeVisible();
@@ -201,7 +249,10 @@ describe('NodeBalancerConfigPanel', () => {
it('renders the relevant helper text for the Source algorithm', () => {
const { getByText, queryByText } = renderWithTheme(
-
+
);
expect(getByText(SOURCE_ALGORITHM_HELPER_TEXT)).toBeVisible();
@@ -215,17 +266,17 @@ describe('NodeBalancerConfigPanel', () => {
it('adds another backend node', () => {
const { getByText } = renderWithTheme(
-
+
);
const addNodeButton = getByText('Add a Node');
fireEvent.click(addNodeButton);
- expect(props.addNode).toHaveBeenCalled();
+ expect(nbConfigPanelMockPropsForTest.addNode).toHaveBeenCalled();
});
it('cannot remove a backend node if there is only one node', () => {
const { queryByText } = renderWithTheme(
-
+
);
expect(queryByText('Remove')).not.toBeInTheDocument();
@@ -233,31 +284,37 @@ describe('NodeBalancerConfigPanel', () => {
it('removes a backend node', () => {
const { getByText } = renderWithTheme(
-
+
);
const removeNodeButton = getByText('Remove');
fireEvent.click(removeNodeButton);
- expect(props.removeNode).toHaveBeenCalled();
+ expect(nbConfigPanelMockPropsForTest.removeNode).toHaveBeenCalled();
});
it('deletes the configuration panel', () => {
const { getByText } = renderWithTheme(
-
+
);
const deleteConfigButton = getByText('Delete');
fireEvent.click(deleteConfigButton);
- expect(props.onDelete).toHaveBeenCalled();
+ expect(nbConfigPanelMockPropsForTest.onDelete).toHaveBeenCalled();
});
it('saves the input after editing the configuration', () => {
const { getByText } = renderWithTheme(
-
+
);
const editConfigButton = getByText('Save');
fireEvent.click(editConfigButton);
- expect(props.onSave).toHaveBeenCalled();
+ expect(nbConfigPanelMockPropsForTest.onSave).toHaveBeenCalled();
});
});
diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.test.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.test.tsx
new file mode 100644
index 00000000000..3a2f0a171aa
--- /dev/null
+++ b/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.test.tsx
@@ -0,0 +1,41 @@
+import * as React from 'react';
+
+import { renderWithTheme } from 'src/utilities/testHelpers';
+
+import NodeBalancerCreate from './NodeBalancerCreate';
+
+// Note: see nodeblaancers-create-in-complex-form.spec.ts for an e2e test of this flow
+describe('NodeBalancerCreate', () => {
+ it('renders all parts of the NodeBalancerCreate page', () => {
+ const { getAllByText, getByLabelText, getByText } = renderWithTheme(
+
+ );
+
+ // confirm nodebalancer fields render
+ expect(getByLabelText('NodeBalancer Label')).toBeVisible();
+ expect(getByLabelText('Add Tags')).toBeVisible();
+ expect(getByLabelText('Region')).toBeVisible();
+
+ // confirm Firewall panel renders
+ expect(getByLabelText('Assign Firewall')).toBeVisible();
+ expect(getByText('Create Firewall')).toBeVisible();
+ expect(
+ getByText(
+ /Assign an existing Firewall to this NodeBalancer to control inbound network traffic./
+ )
+ ).toBeVisible();
+
+ // confirm default configuration renders - only confirming headers, as we have additional
+ // unit tests to check the functionality of the NodeBalancerConfigPanel
+ expect(getByText('Configuration - Port 80')).toBeVisible();
+ expect(getByText('Active Health Checks')).toBeVisible();
+ expect(getAllByText('Passive Checks')).toHaveLength(2);
+ expect(getByText('Backend Nodes')).toBeVisible();
+
+ // confirm summary renders
+ expect(getByText('Summary')).toBeVisible();
+ expect(getByText('Configs')).toBeVisible();
+ expect(getByText('Nodes')).toBeVisible();
+ expect(getByText('Create NodeBalancer')).toBeVisible();
+ });
+});
diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDeleteDialog.test.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDeleteDialog.test.tsx
new file mode 100644
index 00000000000..8d23ec37771
--- /dev/null
+++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDeleteDialog.test.tsx
@@ -0,0 +1,43 @@
+import { fireEvent } from '@testing-library/react';
+import * as React from 'react';
+
+import { renderWithTheme } from 'src/utilities/testHelpers';
+
+import { NodeBalancerDeleteDialog } from './NodeBalancerDeleteDialog';
+
+const props = {
+ id: 1,
+ label: 'nb-1',
+ onClose: vi.fn(),
+ open: true,
+};
+
+describe('NodeBalancerDeleteDialog', () => {
+ it('renders the NodeBalancerDeleteDialog', () => {
+ const { getByText } = renderWithTheme(
+
+ );
+
+ expect(
+ getByText('Deleting this NodeBalancer is permanent and can’t be undone.')
+ ).toBeVisible();
+ expect(
+ getByText(
+ 'Traffic will no longer be routed through this NodeBalancer. Please check your DNS settings and either provide the IP address of another active NodeBalancer, or route traffic directly to your Linode.'
+ )
+ ).toBeVisible();
+ expect(getByText('Delete nb-1?')).toBeVisible();
+ expect(getByText('NodeBalancer Label')).toBeVisible();
+ expect(getByText('Cancel')).toBeVisible();
+ expect(getByText('Delete')).toBeVisible();
+ });
+
+ it('calls the onClose function of the dialog', () => {
+ const { getByText } = renderWithTheme(
+
+ );
+
+ fireEvent.click(getByText('Cancel'));
+ expect(props.onClose).toHaveBeenCalled();
+ });
+});
diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerPassiveCheck.test.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerPassiveCheck.test.tsx
new file mode 100644
index 00000000000..50a111b7f85
--- /dev/null
+++ b/packages/manager/src/features/NodeBalancers/NodeBalancerPassiveCheck.test.tsx
@@ -0,0 +1,36 @@
+import { fireEvent } from '@testing-library/react';
+import * as React from 'react';
+
+import { renderWithTheme } from 'src/utilities/testHelpers';
+
+import { nbConfigPanelMockPropsForTest } from './NodeBalancerConfigPanel.test';
+import { PassiveCheck } from './NodeBalancerPassiveCheck';
+
+describe('NodeBalancer PassiveCheck', () => {
+ it('renders the passive check', () => {
+ const { getAllByText, getByText } = renderWithTheme(
+
+ );
+
+ expect(getAllByText('Passive Checks')).toHaveLength(2);
+ expect(
+ getByText(
+ 'Enable passive checks based on observing communication with back-end nodes.'
+ )
+ ).toBeVisible();
+ });
+
+ it('calls onCheckPassiveChange when the check is toggled', () => {
+ const { getByLabelText } = renderWithTheme(
+
+ );
+
+ const passiveChecksToggle = getByLabelText('Passive Checks');
+ expect(passiveChecksToggle).toBeInTheDocument();
+
+ fireEvent.click(passiveChecksToggle);
+ expect(
+ nbConfigPanelMockPropsForTest.onCheckPassiveChange
+ ).toHaveBeenCalled();
+ });
+});
diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.test.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.test.tsx
index 1c55588a837..4a3d494c358 100644
--- a/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.test.tsx
+++ b/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.test.tsx
@@ -1,4 +1,3 @@
-import { NodeBalancer } from '@linode/api-v4';
import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
@@ -8,6 +7,8 @@ import { renderWithTheme } from 'src/utilities/testHelpers';
import { NodeBalancerSelect } from './NodeBalancerSelect';
+import type { NodeBalancer } from '@linode/api-v4';
+
const fakeNodeBalancerData = nodeBalancerFactory.build({
id: 1,
label: 'metadata-test-region',