feat: improve unit test mocking in useTableOrSpreadsheet hook (#40476)

## 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"

### 🔍 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/14733672732>
> Commit: 16afe15ac1a2990e6d6ea5e0e8db6938daedad5c
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=14733672732&attempt=1"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.Sanity`
> Spec:
> <hr>Tue, 29 Apr 2025 15:04:57 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

- **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.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Jacques Ikot 2025-04-30 05:47:00 -07:00 committed by GitHub
parent eb899929bf
commit a8a925005a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 407 additions and 364 deletions

View File

@ -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: <T extends (...args: unknown[]) => 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);
});
});

View File

@ -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 }) => (
<Provider store={store}>
<WidgetQueryGeneratorFormContext.Provider value={contextValue}>
{children}
</WidgetQueryGeneratorFormContext.Provider>
</Provider>
);
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 }) => (
<Provider store={store}>
<WidgetQueryGeneratorFormContext.Provider value={contextValue}>
{children}
</WidgetQueryGeneratorFormContext.Provider>
</Provider>
);
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 }) => (
<Provider store={store}>
<WidgetQueryGeneratorFormContext.Provider value={contextValue}>
{children}
</WidgetQueryGeneratorFormContext.Provider>
</Provider>
);
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 }) => (
<Provider store={store}>
<WidgetQueryGeneratorFormContext.Provider value={contextValue}>
{children}
</WidgetQueryGeneratorFormContext.Provider>
</Provider>
);
const { result } = renderHook(() => useTableOrSpreadsheet(), { wrapper });
// Verify that all spreadsheets are not disabled
result.current.options.forEach((option) => {
expect(option.disabled).toBe(false);
});
});
});