This is an automated email from the ASF dual-hosted git repository.

arafat2198 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git


The following commit(s) were added to refs/heads/master by this push:
     new 9aae7a5c0b HDDS-9791. Add tests for Datanodes page (#7626)
9aae7a5c0b is described below

commit 9aae7a5c0ba19df1777c2bb4ef2329d5a5b79911
Author: Abhishek Pal <[email protected]>
AuthorDate: Thu Jan 2 16:40:26 2025 +0530

    HDDS-9791. Add tests for Datanodes page (#7626)
---
 .../webapps/recon/ozone-recon-web/package.json     |   1 +
 .../webapps/recon/ozone-recon-web/pnpm-lock.yaml   |  12 ++
 .../src/__tests__/datanodes/Datanodes.test.tsx     | 191 +++++++++++++++++++
 .../__tests__/datanodes/DatanodesTable.test.tsx    | 151 +++++++++++++++
 .../src/__tests__/locators/locators.ts             |  13 +-
 .../mocks/datanodeMocks/datanodeResponseMocks.ts   | 212 +++++++++++++++++++++
 .../mocks/datanodeMocks/datanodeServer.ts          |  72 +++++++
 .../src/__tests__/utils/datanodes.utils.tsx        |  23 +++
 .../src/v2/components/search/search.tsx            |   6 +-
 .../src/v2/components/tables/datanodesTable.tsx    |   8 +-
 .../src/v2/pages/datanodes/datanodes.tsx           |  13 +-
 11 files changed, 691 insertions(+), 11 deletions(-)

diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/package.json
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/package.json
index c2c046f112..2b407eaebd 100644
--- 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/package.json
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/package.json
@@ -58,6 +58,7 @@
   "devDependencies": {
     "@testing-library/jest-dom": "^6.4.8",
     "@testing-library/react": "^12.1.5",
+    "@testing-library/user-event": "^14.5.2",
     "@types/react": "16.8.15",
     "@types/react-dom": "16.8.4",
     "@types/react-router-dom": "^5.3.3",
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/pnpm-lock.yaml
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/pnpm-lock.yaml
index dfdbc7cedc..2b28bbf3d5 100644
--- 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/pnpm-lock.yaml
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/pnpm-lock.yaml
@@ -67,6 +67,9 @@ devDependencies:
   '@testing-library/react':
     specifier: ^12.1.5
     version: 12.1.5([email protected])([email protected])
+  '@testing-library/user-event':
+    specifier: ^14.5.2
+    version: 14.5.2(@testing-library/[email protected])
   '@types/react':
     specifier: 16.8.15
     version: 16.8.15
@@ -1222,6 +1225,15 @@ packages:
       react-dom: 16.14.0([email protected])
     dev: true
 
+  /@testing-library/[email protected](@testing-library/[email protected]):
+    resolution: {integrity: 
sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==}
+    engines: {node: '>=12', npm: '>=6'}
+    peerDependencies:
+      '@testing-library/dom': '>=7.21.4'
+    dependencies:
+      '@testing-library/dom': 8.20.1
+    dev: true
+
   /@types/[email protected]:
     resolution: {integrity: 
sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==}
     dev: true
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/datanodes/Datanodes.test.tsx
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/datanodes/Datanodes.test.tsx
new file mode 100644
index 0000000000..a169e1ce34
--- /dev/null
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/datanodes/Datanodes.test.tsx
@@ -0,0 +1,191 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React from 'react';
+import {
+  fireEvent,
+  render,
+  screen,
+  waitFor
+} from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { rest } from "msw";
+import { vi } from 'vitest';
+
+import Datanodes from '@/v2/pages/datanodes/datanodes';
+import * as commonUtils from '@/utils/common';
+import { datanodeServer } from 
'@/__tests__/mocks/datanodeMocks/datanodeServer';
+import { datanodeLocators, searchInputLocator } from 
'@/__tests__/locators/locators';
+import { waitForDNTable } from '@/__tests__/utils/datanodes.utils';
+
+// Mock utility functions
+vi.spyOn(commonUtils, 'showDataFetchError');
+
+vi.mock('@/components/autoReloadPanel/autoReloadPanel', () => ({
+  default: () => <div data-testid="auto-reload-panel" />,
+}));
+vi.mock('@/v2/components/select/multiSelect.tsx', () => ({
+  default: ({ onChange }: { onChange: Function }) => (
+    <select data-testid="multi-select" onChange={(e) => 
onChange(e.target.value)}>
+      <option value="hostname">Hostname</option>
+      <option value="uuid">UUID</option>
+    </select>
+  ),
+}));
+
+describe('Datanodes Component', () => {
+  // Start and stop MSW server before and after all tests
+  beforeAll(() => datanodeServer.listen());
+  afterEach(() => datanodeServer.resetHandlers());
+  afterAll(() => datanodeServer.close());
+
+  test('renders component correctly', () => {
+    render(<Datanodes />);
+
+    expect(screen.getByText(/Datanodes/)).toBeInTheDocument();
+    expect(screen.getByTestId('auto-reload-panel')).toBeInTheDocument();
+    expect(screen.getByTestId('multi-select')).toBeInTheDocument();
+    expect(screen.getByTestId(searchInputLocator)).toBeInTheDocument();
+  });
+
+  test('Renders table with correct number of rows', async () => {
+    render(<Datanodes />);
+
+    // Wait for the data to load
+    const rows = await waitFor(() => 
screen.getAllByTestId(datanodeLocators.datanodeRowRegex));
+    expect(rows).toHaveLength(5); // Based on the mocked DatanodeResponse
+  });
+
+  test('Loads data on mount', async () => {
+    render(<Datanodes />);
+    // Wait for the data to be loaded into the table
+    const dnTable = await waitForDNTable();
+
+    // Ensure the correct data is displayed in the table
+    expect(dnTable).toHaveTextContent('ozone-datanode-1.ozone_default');
+    expect(dnTable).toHaveTextContent('HEALTHY');
+  });
+
+  test('Displays no data message if the datanodes API returns an empty array', 
async () => {
+    datanodeServer.use(
+      rest.get('api/v1/datanodes', (req, res, ctx) => {
+        return res(ctx.status(200), ctx.json({ totalCount: 0, datanodes: [] 
}));
+      })
+    );
+
+    render(<Datanodes />);
+
+    // Wait for the no data message
+    await waitFor(() => expect(screen.getByText('No 
Data')).toBeInTheDocument());
+  });
+
+  test('Handles search input change', async () => {
+    render(<Datanodes />);
+    await waitForDNTable();
+
+    const searchInput = screen.getByTestId(searchInputLocator);
+    fireEvent.change(searchInput, {
+      target: { value: 'ozone-datanode-1' }
+    });
+    // Sleep for 310ms to allow debounced search to take effect
+    await new Promise((r) => { setTimeout(r, 310) });
+    const rows = await waitFor(() => 
screen.getAllByTestId(datanodeLocators.datanodeRowRegex));
+    await waitFor(() => expect(rows).toHaveLength(1));
+  });
+
+  test('Handles case-sensitive search', async () => {
+    render(<Datanodes />);
+    await waitForDNTable();
+
+    const searchInput = screen.getByTestId(searchInputLocator);
+    fireEvent.change(searchInput, {
+      target: { value: 'DataNode' }
+    });
+    await waitFor(() => expect(searchInput).toHaveValue('DataNode'));
+    // Sleep for 310ms to allow debounced search to take effect
+    await new Promise((r) => { setTimeout(r, 310) })
+
+    const rows = await waitFor(() => 
screen.getAllByTestId(datanodeLocators.datanodeRowRegex));
+    expect(rows).toHaveLength(1);
+  })
+
+  test('Displays a message when no results match the search term', async () => 
{
+    render(<Datanodes />);
+    const searchInput = screen.getByTestId(searchInputLocator);
+
+    // Type a term that doesn't match any datanode
+    fireEvent.change(searchInput, {
+      target: { value: 'nonexistent-datanode' }
+    });
+
+    // Verify that no results message is displayed
+    await waitFor(() => expect(screen.getByText('No 
Data')).toBeInTheDocument());
+  });
+
+  // Since this is a static response, even if we remove we will not get the 
truncated response from backend
+  // i.e response with the removed DN. So the table will always have the value 
even if we remove it
+  // causing this test to fail
+  test.skip('Shows modal on row selection and confirms removal', async () => {
+    render(<Datanodes />);
+
+    // Wait for the data to be loaded into the table
+    await waitForDNTable();
+
+    // Simulate selecting a row
+    // The first checkbox is for the table header "Select All" checkbox -> idx 0
+    // Second checkbox is for the healthy DN                            -> idx 
1
+    // Third checkbox is the active one for Dead DN                     -> idx 
2
+    const checkbox = document.querySelectorAll('input.ant-checkbox-input');
+    userEvent.click(checkbox[0]);
+    // Click the "Remove" button to open the modal
+    await waitFor(() => {
+      // Wait for the button to appear in screen
+      screen.getByTestId(datanodeLocators.datanodeRemoveButton);
+    }).then(() => {
+      userEvent.click(screen.getByText(/Remove/));
+    })
+
+    // Confirm removal in the modal
+    await waitFor(() => {
+      // Wait for the button to appear in screen
+      screen.getByTestId(datanodeLocators.datanodeRemoveModal);
+    }).then(() => {
+      userEvent.click(screen.getByText(/OK/));
+    })
+
+    // Wait for the removal operation to complete
+    await waitFor(() =>
+      
expect(screen.queryByText('ozone-datanode-3.ozone_default')).not.toBeInTheDocument()
+    );
+  });
+
+  test('Handles API errors gracefully by showing error message', async () => {
+    // Set up MSW to return an error for the datanode API
+    datanodeServer.use(
+      rest.get('api/v1/datanodes', (req, res, ctx) => {
+        return res(ctx.status(500), ctx.json({ error: 'Internal Server Error' 
}));
+      })
+    );
+
+    render(<Datanodes />);
+
+    // Wait for the error to be handled
+    await waitFor(() =>
+      expect(commonUtils.showDataFetchError).toHaveBeenCalledWith('AxiosError: 
Request failed with status code 500')
+    );
+  });
+});
\ No newline at end of file
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/datanodes/DatanodesTable.test.tsx
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/datanodes/DatanodesTable.test.tsx
new file mode 100644
index 0000000000..f1be5362ec
--- /dev/null
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/datanodes/DatanodesTable.test.tsx
@@ -0,0 +1,151 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+import { vi } from 'vitest';
+import {
+  fireEvent,
+  render,
+  screen,
+  waitFor
+} from '@testing-library/react';
+
+import { DatanodeTableProps } from '@/v2/types/datanode.types';
+import DatanodesTable from '@/v2/components/tables/datanodesTable';
+import { datanodeServer } from 
'@/__tests__/mocks/datanodeMocks/datanodeServer';
+import { waitForDNTable } from '@/__tests__/utils/datanodes.utils';
+
+const defaultProps: DatanodeTableProps = {
+  loading: false,
+  selectedRows: [],
+  data: [],
+  decommissionUuids: [],
+  searchColumn: 'hostname',
+  searchTerm: '',
+  selectedColumns: [
+    { label: 'Hostname', value: 'hostname' },
+    { label: 'State', value: 'state' },
+  ],
+  handleSelectionChange: vi.fn(),
+};
+
+function getDataWith(name: string, state: "HEALTHY" | "STALE" | "DEAD", uuid: 
number) {
+  return {
+    hostname: name,
+    uuid: uuid,
+    state: state,
+    opState: 'IN_SERVICE',
+    lastHeartbeat: 1728280581608,
+    storageUsed: 4096,
+    storageTotal: 125645656770,
+    storageCommitted: 0,
+    storageRemaining: 114225606656,
+    pipelines: [
+      {
+          "pipelineID": "0f9f7bc0-505e-4428-b148-dd7eac2e8ac2",
+          "replicationType": "RATIS",
+          "replicationFactor": "THREE",
+          "leaderNode": "ozone-datanode-3.ozone_default"
+      },
+      {
+          "pipelineID": "2c23e76e-3f18-4b86-9541-e48bdc152fda",
+          "replicationType": "RATIS",
+          "replicationFactor": "ONE",
+          "leaderNode": "ozone-datanode-1.ozone_default"
+      }
+    ],
+    containers: 8192,
+    openContainers: 8182,
+    leaderCount: 2,
+    version: '0.6.0-SNAPSHOT',
+    setupTime: 1728280539733,
+    revision: '3f9953c0fbbd2175ee83e8f0b4927e45e9c10ac1',
+    buildDate: '2024-10-06T16:41Z',
+    networkLocation: '/default-rack'
+  }
+}
+
+describe('DatanodesTable Component', () => {
+  // Start and stop MSW server before and after all tests
+  beforeAll(() => datanodeServer.listen());
+  afterEach(() => datanodeServer.resetHandlers());
+  afterAll(() => datanodeServer.close());
+
+  test('renders table with data', async () => {
+    render(<DatanodesTable {...defaultProps} data={[]} />);
+
+    // Wait for the table to render
+    waitForDNTable();
+
+    expect(screen.getByTestId('dn-table')).toBeInTheDocument();
+  });
+
+  test('filters data based on search term', async () => {
+    render(
+      <DatanodesTable
+        {...defaultProps}
+        searchTerm="ozone-datanode-1"
+        data={[
+          getDataWith('ozone-datanode-1', 'HEALTHY', 1),
+          getDataWith('ozone-datanode-2', 'STALE', 2)
+        ]}
+      />
+    );
+
+    // Only the matching datanode should be visible
+    expect(screen.getByText('ozone-datanode-1')).toBeInTheDocument();
+    expect(screen.queryByText('ozone-datanode-2')).not.toBeInTheDocument();
+  });
+
+  test('handles row selection', async () => {
+    render(
+      <DatanodesTable
+        {...defaultProps}
+        data={[
+          getDataWith('ozone-datanode-1', 'HEALTHY', 1),
+          getDataWith('ozone-datanode-2', 'DEAD', 2)
+        ]}
+      />
+    );
+
+    // The first checkbox is for the table header "Select All" checkbox -> idx 0
+    // Second checkbox is for the healthy DN                            -> idx 
1
+    // Third checkbox is the active one for Dead DN                     -> idx 
2
+    const checkbox = document.querySelectorAll('input.ant-checkbox-input')[2];
+    fireEvent.click(checkbox);
+
+    expect(defaultProps.handleSelectionChange).toHaveBeenCalledWith([2]);
+  });
+
+  test('disables selection for non-DEAD nodes', async () => {
+    render(
+      <DatanodesTable
+        {...defaultProps}
+        data={[
+          getDataWith('ozone-datanode-1', 'HEALTHY', 1),
+          getDataWith('ozone-datanode-2', 'DEAD', 2)
+        ]}
+      />
+    );
+
+    // Check disabled and enabled rows
+    const checkboxes = document.querySelectorAll('input.ant-checkbox-input');
+    expect(checkboxes[1]).toBeDisabled(); // HEALTHY node
+    expect(checkboxes[2]).not.toBeDisabled(); // DEAD node
+  });
+});
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/locators/locators.ts
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/locators/locators.ts
index 23fbc76870..83b2bc5077 100644
--- 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/locators/locators.ts
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/locators/locators.ts
@@ -37,8 +37,15 @@ export const overviewLocators = {
 }
 
 export const datanodeLocators = {
-  'datanodeContainer': 'datanodes-container',
-  'datanodeMultiSelect': 'datanodes-multiselect'
+  'datanodeMultiSelect': 'dn-multi-select',
+  'datanodeSearchcDropdown': 'search-dropdown',
+  'datanodeSearchInput': 'search-input',
+  'datanodeRemoveButton': 'dn-remove-btn',
+  'datanodeRemoveModal': 'dn-remove-modal',
+  'datanodeTable': 'dn-table',
+  'datanodeRowRegex': /dntable-/,
+  datanodeSearchOption: (label: string) => `search-opt-${label}`,
+  datanodeTableRow: (uuid: string) =>  `dntable-${uuid}`
 }
 
 export const autoReloadPanelLocators = {
@@ -46,3 +53,5 @@ export const autoReloadPanelLocators = {
   'refreshButton': 'autoreload-panel-refresh',
   'toggleSwitch': 'autoreload-panel-switch'
 }
+
+export const searchInputLocator = 'search-input';
\ No newline at end of file
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/mocks/datanodeMocks/datanodeResponseMocks.ts
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/mocks/datanodeMocks/datanodeResponseMocks.ts
new file mode 100644
index 0000000000..887d0b4a27
--- /dev/null
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/mocks/datanodeMocks/datanodeResponseMocks.ts
@@ -0,0 +1,212 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export const DatanodeResponse = {
+  "totalCount": 5,
+  "datanodes": [
+      {
+          "uuid": "1",
+          "hostname": "ozone-datanode-1.ozone_default",
+          "state": "HEALTHY",
+          "opState": "IN_SERVICE",
+          "lastHeartbeat": 1728280581608,
+          "storageReport": {
+              "capacity": 125645656770,
+              "used": 4096,
+              "remaining": 114225606656,
+              "committed": 0
+          },
+          "pipelines": [
+              {
+                  "pipelineID": "0f9f7bc0-505e-4428-b148-dd7eac2e8ac2",
+                  "replicationType": "RATIS",
+                  "replicationFactor": "THREE",
+                  "leaderNode": "ozone-datanode-3.ozone_default"
+              },
+              {
+                  "pipelineID": "2c23e76e-3f18-4b86-9541-e48bdc152fda",
+                  "replicationType": "RATIS",
+                  "replicationFactor": "ONE",
+                  "leaderNode": "ozone-datanode-1.ozone_default"
+              }
+          ],
+          "leaderCount": 1,
+          "version": "2.0.0-SNAPSHOT",
+          "setupTime": 1728280539733,
+          "revision": "3f9953c0fbbd2175ee83e8f0b4927e45e9c10ac1",
+          "buildDate": "2024-10-06T16:41Z",
+          "layoutVersion": 8,
+          "networkLocation": "/default-rack"
+      },
+      {
+          "uuid": "3",
+          "hostname": "ozone-datanode-3.ozone_default",
+          "state": "DEAD",
+          "opState": "IN_SERVICE",
+          "lastHeartbeat": 1728280582060,
+          "storageReport": {
+              "capacity": 125645656770,
+              "used": 4096,
+              "remaining": 114225623040,
+              "committed": 0
+          },
+          "pipelines": [
+              {
+                  "pipelineID": "9c5bbf5e-62da-4d4a-a6ad-cb63d9f6aa6f",
+                  "replicationType": "RATIS",
+                  "replicationFactor": "ONE",
+                  "leaderNode": "ozone-datanode-3.ozone_default"
+              },
+              {
+                  "pipelineID": "0f9f7bc0-505e-4428-b148-dd7eac2e8ac2",
+                  "replicationType": "RATIS",
+                  "replicationFactor": "THREE",
+                  "leaderNode": "ozone-datanode-3.ozone_default"
+              }
+          ],
+          "leaderCount": 2,
+          "version": "1.5.0-SNAPSHOT",
+          "setupTime": 1728280539726,
+          "revision": "3f9953c0fbbd2175ee83e8f0b4927e45e9c10ac1",
+          "buildDate": "2024-10-06T16:41Z",
+          "layoutVersion": 8,
+          "networkLocation": "/default-rack"
+      },
+      {
+          "uuid": "4",
+          "hostname": "ozone-datanode-4.ozone_default",
+          "state": "HEALTHY",
+          "opState": "DECOMMISSIONING",
+          "lastHeartbeat": 1728280581614,
+          "storageReport": {
+              "capacity": 125645656770,
+              "used": 4096,
+              "remaining": 114225541120,
+              "committed": 0
+          },
+          "pipelines": [
+              {
+                  "pipelineID": "4092a584-5c2f-40c6-98e5-ce9a9246e65d",
+                  "replicationType": "RATIS",
+                  "replicationFactor": "ONE",
+                  "leaderNode": "ozone-datanode-4.ozone_default"
+              }
+          ],
+          "leaderCount": 1,
+          "version": "2.0.0-SNAPSHOT",
+          "setupTime": 1728280540325,
+          "revision": "3f9953c0fbbd2175ee83e8f0b4927e45e9c10ac1",
+          "buildDate": "2024-10-06T16:41Z",
+          "layoutVersion": 8,
+          "networkLocation": "/default-rack"
+      },
+      {
+          "uuid": "2",
+          "hostname": "ozone-datanode-2.ozone_default",
+          "state": "STALE",
+          "opState": "IN_SERVICE",
+          "lastHeartbeat": 1728280581594,
+          "storageReport": {
+              "capacity": 125645656770,
+              "used": 4096,
+              "remaining": 114225573888,
+              "committed": 0
+          },
+          "pipelines": [
+              {
+                  "pipelineID": "20a874e4-790b-4312-8fc2-ca53846dba0f",
+                  "replicationType": "RATIS",
+                  "replicationFactor": "ONE",
+                  "leaderNode": "ozone-datanode-2.ozone_default"
+              }
+          ],
+          "leaderCount": 1,
+          "version": "2.0.0-SNAPSHOT",
+          "setupTime": 1728280539745,
+          "revision": "3f9953c0fbbd2175ee83e8f0b4927e45e9c10ac1",
+          "buildDate": "2024-10-06T16:41Z",
+          "layoutVersion": 8,
+          "networkLocation": "/default-rack"
+      },
+      {
+          "uuid": "5",
+          "hostname": "ozone-DataNode-5.ozone_default",
+          "state": "DEAD",
+          "opState": "DECOMMISSIONED",
+          "lastHeartbeat": 1728280582055,
+          "storageReport": {
+              "capacity": 125645656770,
+              "used": 4096,
+              "remaining": 114225614848,
+              "committed": 0
+          },
+          "pipelines": [
+              {
+                  "pipelineID": "0f9f7bc0-505e-4428-b148-dd7eac2e8ac2",
+                  "replicationType": "RATIS",
+                  "replicationFactor": "THREE",
+                  "leaderNode": "ozone-datanode-3.ozone_default"
+              },
+              {
+                  "pipelineID": "67c973a0-722a-403a-8893-b8a5faaed7f9",
+                  "replicationType": "RATIS",
+                  "replicationFactor": "ONE",
+                  "leaderNode": "ozone-datanode-5.ozone_default"
+              }
+          ],
+          "leaderCount": 1,
+          "version": "2.0.0-SNAPSHOT",
+          "setupTime": 1728280539866,
+          "revision": "3f9953c0fbbd2175ee83e8f0b4927e45e9c10ac1",
+          "buildDate": "2024-10-06T16:41Z",
+          "layoutVersion": 8,
+          "networkLocation": "/default-rack"
+      }
+  ]
+}
+
+export const NullDatanodeResponse = {
+  "totalCount": null,
+  "datanodes": [
+      {
+          "uuid": null,
+          "hostname": null,
+          "state": null,
+          "opState": null,
+          "lastHeartbeat": null,
+          "storageReport": null,
+          "pipelines": null,
+          "leaderCount": null,
+          "version": null,
+          "setupTime": null,
+          "revision": null,
+          "buildDate": null,
+          "layoutVersion": null,
+          "networkLocation": null
+      }
+  ]
+}
+
+export const NullDatanodes = {
+  "totalCount": null,
+  "datanodes": null
+}
+
+export const DecommissionInfo = {
+  "DatanodesDecommissionInfo": []
+}
\ No newline at end of file
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/mocks/datanodeMocks/datanodeServer.ts
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/mocks/datanodeMocks/datanodeServer.ts
new file mode 100644
index 0000000000..a7b11f4297
--- /dev/null
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/mocks/datanodeMocks/datanodeServer.ts
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { setupServer } from "msw/node";
+import { rest } from "msw";
+
+import * as mockResponses from "./datanodeResponseMocks";
+
+const handlers = [
+  rest.get("api/v1/datanodes", (req, res, ctx) => {
+    return res(
+      ctx.status(200),
+      ctx.json(mockResponses.DatanodeResponse)
+    );
+  }),
+  rest.get("api/v1/datanodes/decommission/info", (req, res, ctx) => {
+    return res(
+      ctx.status(200),
+      ctx.json(mockResponses.DecommissionInfo)
+    );
+  })
+];
+
+const nullDatanodeResponseHandler = [
+  rest.get("api/v1/datanodes", (req, res, ctx) => {
+    return res(
+      ctx.status(200),
+      ctx.json(mockResponses.NullDatanodeResponse)
+    );
+  }),
+  rest.get("api/v1/datanodes/decommission/info", (req, res, ctx) => {
+    return res(
+      ctx.status(200),
+      ctx.json(mockResponses.DecommissionInfo)
+    );
+  })
+]
+
+const nullDatanodeHandler = [
+  rest.get("api/v1/datanodes", (req, res, ctx) => {
+    return res(
+      ctx.status(200),
+      ctx.json(mockResponses.NullDatanodes)
+    );
+  }),
+  rest.get("api/v1/datanodes/decommission/info", (req, res, ctx) => {
+    return res(
+      ctx.status(200),
+      ctx.json(mockResponses.DecommissionInfo)
+    );
+  })
+]
+
+//This will configure a request mocking server using MSW
+export const datanodeServer = setupServer(...handlers);
+export const nullDatanodeResponseServer = 
setupServer(...nullDatanodeResponseHandler);
+export const nullDatanodeServer = setupServer(...nullDatanodeHandler);
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/utils/datanodes.utils.tsx
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/utils/datanodes.utils.tsx
new file mode 100644
index 0000000000..721a09b895
--- /dev/null
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/__tests__/utils/datanodes.utils.tsx
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { waitFor, screen } from "@testing-library/react";
+
+export const waitForDNTable = async () => {
+  return waitFor(() => screen.getByTestId('dn-table'));
+}
\ No newline at end of file
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/search/search.tsx
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/search/search.tsx
index d320fd659a..ed09b21817 100644
--- 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/search/search.tsx
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/search/search.tsx
@@ -55,7 +55,8 @@ const Search: React.FC<SearchProps> = ({
       suffixIcon={(searchOptions.length > 1) ? <DownOutlined/> : null}
       defaultValue={searchColumn}
       options={searchOptions}
-      onChange={onChange} />)
+      onChange={onChange}
+      data-testid='search-dropdown'/>)
     : null
 
   return (
@@ -69,7 +70,8 @@ const Search: React.FC<SearchProps> = ({
       size='middle'
       style={{
         maxWidth: 400
-      }}/>
+      }}
+      data-testid='search-input'/>
   )
 }
 
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/datanodesTable.tsx
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/datanodesTable.tsx
index 494d898509..17e6048f7e 100644
--- 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/datanodesTable.tsx
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/components/tables/datanodesTable.tsx
@@ -16,7 +16,7 @@
  * limitations under the License.
  */
 
-import React from 'react';
+import React, { HTMLAttributes } from 'react';
 import moment from 'moment';
 import { Popover, Tooltip } from 'antd'
 import {
@@ -306,7 +306,11 @@ const DatanodesTable: React.FC<DatanodeTableProps> = ({
         rowKey='uuid'
         pagination={paginationConfig}
         scroll={{ x: 'max-content', scrollToFirstRowOnChange: true }}
-        locale={{ filterTitle: '' }} />
+        locale={{ filterTitle: '' }}
+        onRow={(record: Datanode) => ({
+          'data-testid': `dntable-${record.uuid}`
+        } as HTMLAttributes<HTMLElement>)}
+        data-testid='dn-table' />
     </div>
   );
 }
diff --git 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/datanodes/datanodes.tsx
 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/datanodes/datanodes.tsx
index fe22d08daf..33dd661d97 100644
--- 
a/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/datanodes/datanodes.tsx
+++ 
b/hadoop-ozone/recon/src/main/resources/webapps/recon/ozone-recon-web/src/v2/pages/datanodes/datanodes.tsx
@@ -246,7 +246,8 @@ const Datanodes: React.FC<{}> = () => {
                 onChange={handleColumnChange}
                 onTagClose={() => { }}
                 fixedColumn='hostname'
-                columnLength={columnOptions.length} />
+                columnLength={columnOptions.length}
+                data-testid='dn-multi-select' />
               {selectedRows.length > 0 &&
                 <Button
                   type="primary"
@@ -256,7 +257,8 @@ const Datanodes: React.FC<{}> = () => {
                     borderColor: '#FF4D4E'
                   }}
                   loading={loading}
-                  onClick={() => { setModalOpen(true) }}> Remove
+                  onClick={() => { setModalOpen(true) }}
+                  data-testid='dn-remove-btn'> Remove
                 </Button>
               }
             </div>
@@ -271,7 +273,7 @@ const Datanodes: React.FC<{}> = () => {
               onChange={(value) => {
                 setSearchTerm('');
                 setSearchColumn(value as 'hostname' | 'uuid' | 'version' | 
'revision')
-              }} />
+              }}/>
           </div>
           <DatanodesTable
             loading={loading}
@@ -281,7 +283,7 @@ const Datanodes: React.FC<{}> = () => {
             searchColumn={searchColumn}
             searchTerm={debouncedSearch}
             handleSelectionChange={handleSelectionChange}
-            decommissionUuids={decommissionUuids} />
+            decommissionUuids={decommissionUuids}/>
         </div>
       </div>
       <Modal
@@ -296,7 +298,8 @@ const Datanodes: React.FC<{}> = () => {
               margin: '0px 0px 5px 0px',
               fontSize: '16px',
               fontWeight: 'bold'
-            }}>
+            }}
+          data-testid='dn-remove-modal'>
               <WarningFilled className='icon-warning' style={{paddingRight: 
'8px'}}/>
               Stop Tracking Datanode
             </div>


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to