PromucFlow_constructor/app/client/src/widgets/TableWidgetV2/component/TableContext.test.tsx
Rahul Barwal 82294f9f85
chore: refactor table widget UI code to use central context to reduce props drilling (#39367)
## Description
Refactor TableWidget for Improved Organization, Context Management &
Rendering Efficiency

### Overview

This PR restructures the TableWidget to enhance code organization,
improve context management, and optimize rendering performance. Key
improvements include extracting reusable components, introducing a
shared TableContext, and simplifying table rendering logic.

### Key Changes
 Improved Table Header Organization & Context Management
 Refactored Table & Virtualization Logic
 Simplified Header Components
 Enhanced Empty Row Handling
 Reorganized Core Components(Static table, virtual table)

### Why These Changes?
• Improves maintainability and readability by reducing prop drilling and
redundant code.
• Enhances performance through better state management and rendering
optimizations.
• Provides a scalable structure for future improvements in the
TableWidget.


Fixes #39308
_or_  
Fixes `Issue URL`
> [!WARNING]  
> _If no issue exists, please create an issue first, and check with the
maintainers if the issue is valid._

## Automation

/ok-to-test tags="@tag.Table, @tag.Sanity, @tag.Datasource"

### 🔍 Cypress test results
<!-- This is an auto-generated comment: Cypress test results  -->
> [!TIP]
> 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/13761069297>
> Commit: 524a8464a65576c9da485f686e6598eba38358a5
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=13761069297&attempt=2"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.Table, @tag.Sanity, @tag.Datasource`
> Spec:
> <hr>Mon, 10 Mar 2025 12:31:27 UTC
<!-- end of auto-generated comment: Cypress test results  -->


## Communication
Should the DevRel and Marketing teams inform users about this change?
- [ ] Yes
- [ ] No


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Summary by CodeRabbit

- **New Features**
- Introduced new virtualized table components that enhance dynamic
scrolling and table interactions.
  - Added a new table constant to ensure smoother scrolling behavior.
- Implemented a centralized table state provider to consolidate table
functionality.
- Added a new `BannerNActions` component for improved header action
management.
- Added a new `StaticTableBodyComponent` for rendering static table
bodies.

- **Refactor**
- Streamlined static table rendering along with header and cell
interactions.
- Simplified component interfaces by shifting from prop-based to
context-driven state management.
- Enhanced the `Actions` component to utilize context for state
management.
  - Refactored the `Table` component to improve clarity and efficiency.

- **Tests**
- Expanded and improved test coverage to ensure robust rendering, error
handling, and performance.
- Introduced tests for new components such as `StaticTableBodyComponent`
and `BannerNActions`.
- Updated tests to reflect changes in context management and component
structure.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-03-13 11:23:05 +05:30

310 lines
8.2 KiB
TypeScript

import React, { useContext } from "react";
import { render, screen } from "@testing-library/react";
import TestRenderer from "react-test-renderer";
import {
TableProvider,
TableContext,
useAppsmithTable,
type TableContextState,
} from "./TableContext";
import { CompactModeTypes, TABLE_SIZES } from "./Constants";
// Mock data and props
const mockTableProviderProps = {
width: 800,
height: 400,
pageSize: 10,
isHeaderVisible: true,
compactMode: CompactModeTypes.DEFAULT,
currentPageIndex: 0,
pageCount: 5,
pageOptions: [0, 1, 2, 3, 4],
headerGroups: [],
totalColumnsWidth: 800,
isResizingColumn: { current: false },
prepareRow: jest.fn(),
rowSelectionState: null,
subPage: [],
handleAllRowSelectClick: jest.fn(),
getTableBodyProps: jest.fn(),
children: null,
// Additional required props
widgetId: "table-widget-1",
widgetName: "Table1",
searchKey: "",
isLoading: false,
columns: [],
data: [],
editMode: false,
editableCell: {
column: "column1",
row: 0,
index: 0,
value: "",
initialValue: "",
inputValue: "",
__originalIndex__: 0,
},
sortTableColumn: jest.fn(),
handleResizeColumn: jest.fn(),
handleReorderColumn: jest.fn(),
selectTableRow: jest.fn(),
pageNo: 0,
updatePageNo: jest.fn(),
nextPageClick: jest.fn(),
prevPageClick: jest.fn(),
serverSidePaginationEnabled: false,
selectedRowIndex: -1,
selectedRowIndices: [],
disableDrag: jest.fn(),
enableDrag: jest.fn(),
toggleAllRowSelect: jest.fn(),
triggerRowSelection: false,
searchTableData: jest.fn(),
filters: [],
applyFilter: jest.fn(),
delimiter: ",",
accentColor: "#000000",
isSortable: true,
multiRowSelection: false,
columnWidthMap: {},
// Additional required props from latest error
borderRadius: "0px",
boxShadow: "none",
onBulkEditDiscard: jest.fn(),
onBulkEditSave: jest.fn(),
primaryColumns: {},
derivedColumns: {},
sortOrder: { column: "", order: null },
transientTableData: {},
isEditableCellsValid: {},
selectColumnFilterText: {},
isAddRowInProgress: false,
newRow: {},
firstEditableColumnIdByOrder: "",
enableServerSideFiltering: false,
onTableFilterUpdate: "",
customIsLoading: false,
customIsLoadingValue: false,
infiniteScrollEnabled: false,
// Final set of required props
allowAddNewRow: false,
onAddNewRow: jest.fn(),
onAddNewRowAction: jest.fn(),
disabledAddNewRowSave: false,
addNewRowValidation: {},
onAddNewRowSave: jest.fn(),
onAddNewRowDiscard: jest.fn(),
// Last set of required props
showConnectDataOverlay: false,
onConnectData: jest.fn(),
isInfiniteScrollEnabled: false,
};
// Test components
interface TestChildProps {
tableContext: TableContextState | undefined;
}
const TestChild = (props: TestChildProps) => {
return (
<div>{Object.keys(props.tableContext as TableContextState).join(",")}</div>
);
};
const TestParent = () => {
const tableContext = useContext(TableContext);
return <TestChild tableContext={tableContext} />;
};
describe("TableContext", () => {
describe("context values and usage", () => {
it("provides correct context values to children", async () => {
const testRenderer = TestRenderer.create(
<TableProvider {...mockTableProviderProps}>
<TestParent />
</TableProvider>,
);
const testInstance = testRenderer.root;
const expectedKeys = [
"width",
"height",
"pageSize",
"isHeaderVisible",
"compactMode",
"currentPageIndex",
"pageCount",
"pageOptions",
"headerGroups",
"totalColumnsWidth",
"isResizingColumn",
"prepareRow",
"rowSelectionState",
"subPage",
"handleAllRowSelectClick",
"getTableBodyProps",
"scrollContainerStyles",
"tableSizes",
"widgetId",
"widgetName",
"searchKey",
"isLoading",
"columns",
"data",
"editMode",
"editableCell",
"sortTableColumn",
"handleResizeColumn",
"handleReorderColumn",
"selectTableRow",
"pageNo",
"updatePageNo",
"nextPageClick",
"prevPageClick",
"serverSidePaginationEnabled",
"selectedRowIndex",
"selectedRowIndices",
"disableDrag",
"enableDrag",
"toggleAllRowSelect",
"triggerRowSelection",
"searchTableData",
"filters",
"applyFilter",
"delimiter",
"accentColor",
"isSortable",
"multiRowSelection",
"columnWidthMap",
"borderRadius",
"boxShadow",
"onBulkEditDiscard",
"onBulkEditSave",
"primaryColumns",
"derivedColumns",
"sortOrder",
"transientTableData",
"isEditableCellsValid",
"selectColumnFilterText",
"isAddRowInProgress",
"newRow",
"firstEditableColumnIdByOrder",
"enableServerSideFiltering",
"onTableFilterUpdate",
"customIsLoading",
"customIsLoadingValue",
"infiniteScrollEnabled",
"allowAddNewRow",
"onAddNewRow",
"onAddNewRowAction",
"disabledAddNewRowSave",
"addNewRowValidation",
"onAddNewRowSave",
"onAddNewRowDiscard",
"showConnectDataOverlay",
"onConnectData",
"isInfiniteScrollEnabled",
].sort();
const result = Object.keys(
(await testInstance.findByType(TestChild)).props.tableContext,
).sort();
expect(result).toEqual(expectedKeys);
});
it("throws error when useAppsmithTable is used outside TableProvider", () => {
const TestComponent = () => {
useAppsmithTable();
return null;
};
const consoleError = jest
.spyOn(console, "error")
.mockImplementation(() => {});
expect(() => render(<TestComponent />)).toThrow(
"useTable must be used within a TableProvider",
);
consoleError.mockRestore();
});
});
describe("scrollContainerStyles", () => {
it("calculates correct scrollContainerStyles when header is visible", async () => {
const testRenderer = TestRenderer.create(
<TableProvider {...mockTableProviderProps}>
<TestParent />
</TableProvider>,
);
const testInstance = testRenderer.root;
const context = (await testInstance.findByType(TestChild)).props
.tableContext;
expect(context.scrollContainerStyles).toEqual({
height: 352, // 400 - 40 - 8 (height - TABLE_HEADER_HEIGHT - TABLE_SCROLLBAR_HEIGHT)
width: 800,
});
});
it("calculates correct scrollContainerStyles when header is not visible", async () => {
const testRenderer = TestRenderer.create(
<TableProvider
{...{ ...mockTableProviderProps, isHeaderVisible: false }}
>
<TestParent />
</TableProvider>,
);
const testInstance = testRenderer.root;
const context = (await testInstance.findByType(TestChild)).props
.tableContext;
expect(context.scrollContainerStyles).toEqual({
height: 390, // 400 - 8 - 2 (height - TABLE_SCROLLBAR_HEIGHT - SCROLL_BAR_OFFSET)
width: 800,
});
});
});
it("provides correct tableSizes based on compactMode", async () => {
const testRenderer = TestRenderer.create(
<TableProvider {...mockTableProviderProps}>
<TestParent />
</TableProvider>,
);
const testInstance = testRenderer.root;
const context = (await testInstance.findByType(TestChild)).props
.tableContext;
expect(context.tableSizes).toEqual(
TABLE_SIZES[mockTableProviderProps.compactMode],
);
});
it("memoizes context value and scrollContainerStyles", () => {
const { rerender } = render(
<TableProvider {...mockTableProviderProps}>
<TestParent />
</TableProvider>,
);
const firstRender = screen.getByText(/.+/);
const firstText = firstRender.textContent;
// Rerender with same props
rerender(
<TableProvider {...mockTableProviderProps}>
<TestParent />
</TableProvider>,
);
const secondRender = screen.getByText(/.+/);
const secondText = secondRender.textContent;
expect(firstText).toBe(secondText);
});
});