From a8a925005af697be6e2bd3dac48bf2ca4630d7d4 Mon Sep 17 00:00:00 2001 From: Jacques Ikot Date: Wed, 30 Apr 2025 05:47:00 -0700 Subject: [PATCH] feat: improve unit test mocking in useTableOrSpreadsheet hook (#40476) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description The useTableOrSpreadsheet hook unit test consistently failed on EE due to complex mocking and missing state values. This PR improves the test cases using a simpler state object to test the hook. Fixes #`Issue Number` _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.Sanity" ### :mag: Cypress test results > [!TIP] > 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉 > Workflow run: > Commit: 16afe15ac1a2990e6d6ea5e0e8db6938daedad5c > Cypress dashboard. > Tags: `@tag.Sanity` > Spec: >
Tue, 29 Apr 2025 15:04:57 UTC ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [ ] No ## Summary by CodeRabbit - **Tests** - Replaced the previous test suite for the table or spreadsheet dropdown hook with a new set of tests, ensuring correct behavior across SQL, MongoDB, and Google Sheets datasources. The new tests verify that options are enabled or disabled appropriately based on datasource type and configuration. --- .../useTableOrSpreadsheet.test.skip.ts | 364 ---------------- .../useTableOrSpreadsheet.test.tsx | 407 ++++++++++++++++++ 2 files changed, 407 insertions(+), 364 deletions(-) delete mode 100644 app/client/src/components/editorComponents/WidgetQueryGeneratorForm/CommonControls/TableOrSpreadsheetDropdown/useTableOrSpreadsheet.test.skip.ts create mode 100644 app/client/src/components/editorComponents/WidgetQueryGeneratorForm/CommonControls/TableOrSpreadsheetDropdown/useTableOrSpreadsheet.test.tsx diff --git a/app/client/src/components/editorComponents/WidgetQueryGeneratorForm/CommonControls/TableOrSpreadsheetDropdown/useTableOrSpreadsheet.test.skip.ts b/app/client/src/components/editorComponents/WidgetQueryGeneratorForm/CommonControls/TableOrSpreadsheetDropdown/useTableOrSpreadsheet.test.skip.ts deleted file mode 100644 index a20431bc13..0000000000 --- a/app/client/src/components/editorComponents/WidgetQueryGeneratorForm/CommonControls/TableOrSpreadsheetDropdown/useTableOrSpreadsheet.test.skip.ts +++ /dev/null @@ -1,364 +0,0 @@ -import { renderHook } from "@testing-library/react-hooks"; -import type { DatasourceTable } from "entities/Datasource"; -import { PluginPackageName } from "entities/Plugin"; -import { useTableOrSpreadsheet } from "./useTableOrSpreadsheet"; - -// Mock applicationSelectors -jest.mock("ee/selectors/applicationSelectors", () => ({ - getApplications: jest.fn(() => []), - getCurrentApplication: jest.fn(() => ({})), - getApplicationSearchKeyword: jest.fn(), - getIsDeletingApplication: jest.fn(() => false), - getIsDuplicatingApplication: jest.fn(() => false), - getIsImportingApplication: jest.fn(() => false), -})); - -// Mock AppState and create MockStore -jest.mock("store", () => ({ - store: { - getState: jest.fn(() => ({ - entities: { - app: { - mode: "EDIT", - }, - }, - ui: { - applications: { - searchKeyword: "", - deletingApplication: false, - }, - }, - })), - }, -})); - -// Mock pageListSelectors -jest.mock("selectors/pageListSelectors", () => ({ - getIsGeneratingTemplatePage: jest.fn(() => false), - getIsGeneratePageModalOpen: jest.fn(() => false), - getApplicationLastModifiedTime: jest.fn(), - getCurrentApplicationId: jest.fn(), - getCurrentPageId: jest.fn(), - getPageById: jest.fn(), - getPageList: jest.fn(() => []), - getPageListAsOptions: jest.fn(() => []), - getPageListState: jest.fn(() => ({ - isGeneratingTemplatePage: false, - isGeneratePageModalOpen: false, - })), -})); - -// Mock UI selectors -jest.mock("selectors/ui", () => ({ - getSelectedAppTheme: jest.fn(), - getAppThemes: jest.fn(() => []), - getSelectedAppThemeColor: jest.fn(), - getIsDatasourceInViewMode: jest.fn(() => false), - getDatasourceCollapsibleState: jest.fn(() => ({})), - getIsInOnboardingFlow: jest.fn(() => false), -})); - -// Mock datasourceActions -jest.mock("actions/datasourceActions", () => ({ - fetchGheetSheets: jest.fn(() => ({ type: "FETCH_GSHEET_SHEETS" })), -})); - -// Mock editor selectors -jest.mock("selectors/editorSelectors", () => ({ - getWidgets: jest.fn(() => ({})), - getWidgetsMeta: jest.fn(() => ({})), - getWidgetsForImport: jest.fn(() => ({})), -})); - -// Mock Reselect -jest.mock("reselect", () => ({ - createSelector: jest.fn((selectors, resultFunc) => { - if (typeof resultFunc === "function") { - return resultFunc(); - } - - return jest.fn(); - }), -})); - -// Mock selectors -jest.mock("selectors/dataTreeSelectors", () => ({ - getLayoutSystemType: jest.fn(), - getIsMobileBreakPoint: jest.fn(), -})); - -// Mock redux -jest.mock("react-redux", () => ({ - useSelector: jest.fn(), - useDispatch: () => jest.fn(), -})); - -// Mock redux-form -jest.mock("redux-form", () => ({ - getFormValues: jest.fn(), - Field: jest.fn(), - reduxForm: jest.fn(), -})); - -// Mock ee/selectors/entitiesSelector -jest.mock("ee/selectors/entitiesSelector", () => ({ - getDatasource: jest.fn(), - getDatasourceLoading: jest.fn(), - getDatasourceStructureById: jest.fn(), - getIsFetchingDatasourceStructure: jest.fn(), - getPluginPackageFromDatasourceId: jest.fn(), -})); - -// Mock selectors/datasourceSelectors -jest.mock("selectors/datasourceSelectors", () => ({ - getGsheetSpreadsheets: jest.fn(() => jest.fn()), - getIsFetchingGsheetSpreadsheets: jest.fn(), -})); - -// Mock selectors/oneClickBindingSelectors -jest.mock("selectors/oneClickBindingSelectors", () => ({ - getisOneClickBindingConnectingForWidget: jest.fn(() => jest.fn()), -})); - -// Mock sagas/selectors -jest.mock("sagas/selectors", () => ({ - getWidget: jest.fn(), -})); - -// Mock utils -jest.mock("utils/editorContextUtils", () => ({ - isGoogleSheetPluginDS: jest.fn( - (plugin) => plugin === PluginPackageName.GOOGLE_SHEETS, - ), - isMongoDBPluginDS: jest.fn((plugin) => plugin === PluginPackageName.MONGO), -})); - -// Mock utils/helpers -jest.mock("utils/helpers", () => ({ - getAppMode: jest.fn(() => "EDIT"), - isEllipsisActive: jest.fn(), - modText: jest.fn(() => "Ctrl +"), - isMacOrIOS: jest.fn(() => false), - shiftText: jest.fn(() => "Shift +"), -})); - -// Mock WidgetOperationUtils -jest.mock("sagas/WidgetOperationUtils", () => ({})); - -// Mock WidgetUtils -jest.mock("widgets/WidgetUtils", () => ({})); - -// Mock environmentSelectors -jest.mock("ee/selectors/environmentSelectors", () => ({ - getCurrentEnvironmentId: jest.fn(), - getCurrentEnvironmentName: jest.fn(), -})); - -// Mock the context -jest.mock("react", () => { - const originalModule = jest.requireActual("react"); - - return { - ...originalModule, - useContext: () => ({ - config: { datasource: "test-ds-id", table: "" }, - propertyName: "test-property", - updateConfig: jest.fn(), - widgetId: "test-widget-id", - }), - useCallback: unknown>(fn: T) => fn, - // useMemo will be mocked in each test - useMemo: jest.fn(), - }; -}); - -// Import after all mocks are set up -import * as React from "react"; -import { useSelector } from "react-redux"; - -// Create a simplified test -describe("useTableOrSpreadsheet", () => { - const mockUseSelector = useSelector as jest.Mock; - const mockUseMemo = React.useMemo as jest.Mock; - - beforeEach(() => { - jest.clearAllMocks(); - mockUseSelector.mockImplementation(() => ({})); - mockUseMemo.mockImplementation((fn) => fn()); - }); - - it("should render without crashing", () => { - // Mock minimum required values - mockUseSelector.mockImplementation(() => ({ - datasourceStructure: { tables: [] }, - selectedDatasourcePluginPackageName: PluginPackageName.POSTGRES, - selectedDatasource: { name: "Test" }, - widget: { widgetName: "Test" }, - })); - - const { result } = renderHook(() => useTableOrSpreadsheet()); - - expect(result.current).toBeTruthy(); - }); - - it("should disable tables without primary keys for non-MongoDB datasources", () => { - const mockTables: DatasourceTable[] = [ - { - name: "table1", - type: "TABLE", - columns: [], - keys: [], - templates: [], - }, - { - name: "table2", - type: "TABLE", - columns: [], - keys: [ - { - name: "id", - type: "primary", - columnNames: ["id"], - fromColumns: ["id"], - }, - ], - templates: [], - }, - ]; - - const mockOptions = [ - { - id: "table1", - label: "table1", - value: "table1", - data: { tableName: "table1" }, - disabled: true, - }, - { - id: "table2", - label: "table2", - value: "table2", - data: { tableName: "table2" }, - disabled: false, - }, - ]; - - mockUseSelector.mockImplementation(() => ({ - selectedDatasourcePluginPackageName: PluginPackageName.POSTGRES, - datasourceStructure: { tables: mockTables }, - widget: { widgetName: "TestWidget" }, - selectedDatasource: { name: "TestDatabase" }, - })); - - mockUseMemo.mockImplementation(() => mockOptions); - - const { result } = renderHook(() => useTableOrSpreadsheet()); - - expect(result.current.options).toHaveLength(2); - expect(result.current.options[0].value).toBe("table1"); - expect(result.current.options[0].disabled).toBe(true); - expect(result.current.options[1].value).toBe("table2"); - expect(result.current.options[1].disabled).toBe(false); - }); - - it("should not disable tables for MongoDB datasources regardless of primary keys", () => { - const mockTables: DatasourceTable[] = [ - { - name: "collection1", - type: "COLLECTION", - columns: [], - keys: [], - templates: [], - }, - { - name: "collection2", - type: "COLLECTION", - columns: [], - keys: [ - { - name: "_id", - type: "primary", - columnNames: ["_id"], - fromColumns: ["_id"], - }, - ], - templates: [], - }, - ]; - - const mockOptions = [ - { - id: "collection1", - label: "collection1", - value: "collection1", - data: { tableName: "collection1" }, - disabled: false, - }, - { - id: "collection2", - label: "collection2", - value: "collection2", - data: { tableName: "collection2" }, - disabled: false, - }, - ]; - - mockUseSelector.mockImplementation(() => ({ - selectedDatasourcePluginPackageName: PluginPackageName.MONGO, - datasourceStructure: { tables: mockTables }, - widget: { widgetName: "TestWidget" }, - selectedDatasource: { name: "TestMongoDB" }, - })); - - mockUseMemo.mockImplementation(() => mockOptions); - - const { result } = renderHook(() => useTableOrSpreadsheet()); - - expect(result.current.options).toHaveLength(2); - expect(result.current.options[0].value).toBe("collection1"); - expect(result.current.options[0].disabled).toBe(false); - expect(result.current.options[1].value).toBe("collection2"); - expect(result.current.options[1].disabled).toBe(false); - }); - - it("should not disable tables for Google Sheets datasource", () => { - const mockOptions = [ - { - id: "sheet1", - label: "Sheet1", - value: "Sheet1", - data: { tableName: "sheet1" }, - disabled: false, - }, - { - id: "sheet2", - label: "Sheet2", - value: "Sheet2", - data: { tableName: "sheet2" }, - disabled: false, - }, - ]; - - mockUseSelector.mockImplementation(() => ({ - selectedDatasourcePluginPackageName: PluginPackageName.GOOGLE_SHEETS, - spreadSheets: { - value: [ - { label: "Sheet1", value: "sheet1" }, - { label: "Sheet2", value: "sheet2" }, - ], - }, - widget: { widgetName: "TestWidget" }, - selectedDatasource: { name: "TestGoogleSheet" }, - })); - - mockUseMemo.mockImplementation(() => mockOptions); - - const { result } = renderHook(() => useTableOrSpreadsheet()); - - expect(result.current.options).toHaveLength(2); - expect(result.current.options[0].value).toBe("Sheet1"); - expect(result.current.options[0].disabled).toBe(false); - expect(result.current.options[1].value).toBe("Sheet2"); - expect(result.current.options[1].disabled).toBe(false); - }); -}); diff --git a/app/client/src/components/editorComponents/WidgetQueryGeneratorForm/CommonControls/TableOrSpreadsheetDropdown/useTableOrSpreadsheet.test.tsx b/app/client/src/components/editorComponents/WidgetQueryGeneratorForm/CommonControls/TableOrSpreadsheetDropdown/useTableOrSpreadsheet.test.tsx new file mode 100644 index 0000000000..ef106936c2 --- /dev/null +++ b/app/client/src/components/editorComponents/WidgetQueryGeneratorForm/CommonControls/TableOrSpreadsheetDropdown/useTableOrSpreadsheet.test.tsx @@ -0,0 +1,407 @@ +import { renderHook } from "@testing-library/react-hooks"; +import React from "react"; +import { Provider } from "react-redux"; +import configureStore from "redux-mock-store"; +import { WidgetQueryGeneratorFormContext } from "../.."; +import { DROPDOWN_VARIANT } from "../DatasourceDropdown/types"; +import { useTableOrSpreadsheet } from "./useTableOrSpreadsheet"; + +const mockStore = configureStore([]); + +describe("useTableOrSpreadsheet", () => { + it("returns correct options and labelText for minimal valid state", () => { + const state = { + entities: { + datasources: { + list: [{ id: "ds1", name: "TestDatasource", pluginId: "plugin1" }], + structure: { + ds1: { + id: "ds1", + tables: [], + }, + }, + fetchingDatasourceStructure: { + ds1: false, + }, + gsheetStructure: { + spreadsheets: { + ds1: [{ id: "123", label: "Sheet1", value: "sheet1" }], + }, + isFetchingSpreadsheets: false, + }, + loading: false, + }, + plugins: { + list: [ + { id: "plugin1", packageName: "TestPlugin", name: "TestPlugin" }, + ], + }, + canvasWidgets: { + Widget1: { widgetName: "Widget1", type: "TABLE_WIDGET" }, + }, + }, + ui: { + oneClickBinding: { + isConnecting: false, + config: { + widgetId: "Widget1", + }, + showOptions: false, + }, + }, + }; + const store = mockStore(state); + + const contextValue = { + config: { + datasource: "ds1", + datasourcePluginType: "DB", + table: "", + datasourcePluginName: "TestPlugin", + datasourceConnectionMode: "READ_WRITE", + alias: {}, + sheet: "", + searchableColumn: "", + tableHeaderIndex: 1, + }, + propertyName: "table", + updateConfig: jest.fn(), + widgetId: "Widget1", + propertyValue: "", + addBinding: jest.fn(), + isSourceOpen: false, + onSourceClose: jest.fn(), + errorMsg: "", + expectedType: "", + sampleData: "", + aliases: [], + otherFields: [], + excludePrimaryColumnFromQueryGeneration: false, + isConnectableToWidget: false, + datasourceDropdownVariant: DROPDOWN_VARIANT.CONNECT_TO_DATASOURCE, // Use the correct enum or string + alertMessage: null, + // Add any other required fields with dummy values + }; + + const wrapper = ({ children }: { children: React.ReactNode }) => ( + + + {children} + + + ); + + const { result } = renderHook(() => useTableOrSpreadsheet(), { wrapper }); + + expect(result.current.labelText).toContain("Select"); + expect(result.current.labelText).toContain("TestDatasource"); + expect(Array.isArray(result.current.options)).toBe(true); + }); + + it("should disable tables without primary keys for non-MongoDB datasources", () => { + const state = { + entities: { + datasources: { + list: [{ id: "ds1", name: "PostgresDB", pluginId: "plugin1" }], + structure: { + ds1: { + id: "ds1", + tables: [ + { + name: "table_with_pk", + keys: [ + { name: "id_pk", type: "primary key", columnNames: ["id"] }, + ], + columns: [{ name: "id", type: "integer" }], + }, + { + name: "table_without_pk", + keys: [], + columns: [{ name: "data", type: "text" }], + }, + ], + }, + }, + fetchingDatasourceStructure: { + ds1: false, + }, + gsheetStructure: { + spreadsheets: { + ds1: [], + }, + isFetchingSpreadsheets: false, + }, + loading: false, + }, + plugins: { + list: [ + { id: "plugin1", packageName: "postgres", name: "PostgreSQL" }, + ], + }, + canvasWidgets: { + Widget1: { widgetName: "Widget1", type: "TABLE_WIDGET" }, + }, + }, + ui: { + oneClickBinding: { + isConnecting: false, + config: { + widgetId: "Widget1", + }, + showOptions: false, + }, + }, + }; + const store = mockStore(state); + + const contextValue = { + config: { + datasource: "ds1", + datasourcePluginType: "DB", + table: "", + datasourcePluginName: "postgres", + datasourceConnectionMode: "READ_WRITE", + alias: {}, + sheet: "", + searchableColumn: "", + tableHeaderIndex: 1, + }, + propertyName: "table", + updateConfig: jest.fn(), + widgetId: "Widget1", + propertyValue: "", + addBinding: jest.fn(), + isSourceOpen: false, + onSourceClose: jest.fn(), + errorMsg: "", + expectedType: "", + sampleData: "", + aliases: [], + otherFields: [], + excludePrimaryColumnFromQueryGeneration: false, + isConnectableToWidget: false, + datasourceDropdownVariant: DROPDOWN_VARIANT.CONNECT_TO_DATASOURCE, + alertMessage: null, + }; + + const wrapper = ({ children }: { children: React.ReactNode }) => ( + + + {children} + + + ); + + const { result } = renderHook(() => useTableOrSpreadsheet(), { wrapper }); + + // Verify that table with primary key is enabled + const tableWithPkOption = result.current.options.find( + (option) => option.value === "table_with_pk", + ); + + expect(tableWithPkOption?.disabled).toBe(false); + + // Verify that table without primary key is disabled + const tableWithoutPkOption = result.current.options.find( + (option) => option.value === "table_without_pk", + ); + + expect(tableWithoutPkOption?.disabled).toBe(true); + }); + + it("should not disable tables for MongoDB datasources regardless of primary keys", () => { + const state = { + entities: { + datasources: { + list: [{ id: "ds1", name: "MongoDB", pluginId: "plugin1" }], + structure: { + ds1: { + id: "ds1", + tables: [ + { + name: "collection1", + keys: [], + columns: [{ name: "_id", type: "objectId" }], + }, + { + name: "collection2", + keys: [], + columns: [{ name: "data", type: "object" }], + }, + ], + }, + }, + fetchingDatasourceStructure: { + ds1: false, + }, + gsheetStructure: { + spreadsheets: { + ds1: [], + }, + isFetchingSpreadsheets: false, + }, + loading: false, + }, + plugins: { + list: [ + { id: "plugin1", packageName: "mongo-plugin", name: "MongoDB" }, + ], + }, + canvasWidgets: { + Widget1: { widgetName: "Widget1", type: "TABLE_WIDGET" }, + }, + }, + ui: { + oneClickBinding: { + isConnecting: false, + config: { + widgetId: "Widget1", + }, + showOptions: false, + }, + }, + }; + const store = mockStore(state); + + const contextValue = { + config: { + datasource: "ds1", + datasourcePluginType: "DB", + table: "", + datasourcePluginName: "mongo", + datasourceConnectionMode: "READ_WRITE", + alias: {}, + sheet: "", + searchableColumn: "", + tableHeaderIndex: 1, + }, + propertyName: "table", + updateConfig: jest.fn(), + widgetId: "Widget1", + propertyValue: "", + addBinding: jest.fn(), + isSourceOpen: false, + onSourceClose: jest.fn(), + errorMsg: "", + expectedType: "", + sampleData: "", + aliases: [], + otherFields: [], + excludePrimaryColumnFromQueryGeneration: false, + isConnectableToWidget: false, + datasourceDropdownVariant: DROPDOWN_VARIANT.CONNECT_TO_DATASOURCE, + alertMessage: null, + }; + + const wrapper = ({ children }: { children: React.ReactNode }) => ( + + + {children} + + + ); + + const { result } = renderHook(() => useTableOrSpreadsheet(), { wrapper }); + + // Verify that all tables for MongoDB are not disabled, regardless of primary keys + result.current.options.forEach((option) => { + expect(option.disabled).toBe(false); + }); + }); + + it("should not disable tables for Google Sheets datasource", () => { + const state = { + entities: { + datasources: { + list: [{ id: "ds1", name: "Sheets", pluginId: "plugin1" }], + structure: { + ds1: { + id: "ds1", + tables: [], + }, + }, + fetchingDatasourceStructure: { + ds1: false, + }, + gsheetStructure: { + spreadsheets: { + ds1: [ + { id: "sheet1", label: "Sheet1", value: "sheet1" }, + { id: "sheet2", label: "Sheet2", value: "sheet2" }, + ], + }, + isFetchingSpreadsheets: false, + }, + loading: false, + }, + plugins: { + list: [ + { + id: "plugin1", + packageName: "google-sheets-plugin", + name: "Google Sheets", + }, + ], + }, + canvasWidgets: { + Widget1: { widgetName: "Widget1", type: "TABLE_WIDGET" }, + }, + }, + ui: { + oneClickBinding: { + isConnecting: false, + config: { + widgetId: "Widget1", + }, + showOptions: false, + }, + }, + }; + const store = mockStore(state); + + const contextValue = { + config: { + datasource: "ds1", + datasourcePluginType: "SAAS", + table: "", + datasourcePluginName: "google-sheets", + datasourceConnectionMode: "READ_WRITE", + alias: {}, + sheet: "", + searchableColumn: "", + tableHeaderIndex: 1, + }, + propertyName: "table", + updateConfig: jest.fn(), + widgetId: "Widget1", + propertyValue: "", + addBinding: jest.fn(), + isSourceOpen: false, + onSourceClose: jest.fn(), + errorMsg: "", + expectedType: "", + sampleData: "", + aliases: [], + otherFields: [], + excludePrimaryColumnFromQueryGeneration: false, + isConnectableToWidget: false, + datasourceDropdownVariant: DROPDOWN_VARIANT.CONNECT_TO_DATASOURCE, + alertMessage: null, + }; + + const wrapper = ({ children }: { children: React.ReactNode }) => ( + + + {children} + + + ); + + const { result } = renderHook(() => useTableOrSpreadsheet(), { wrapper }); + + // Verify that all spreadsheets are not disabled + result.current.options.forEach((option) => { + expect(option.disabled).toBe(false); + }); + }); +});