chore: Adding specs/tests for space distribution and copy paste sagas (#34063)

[![workerB](https://img.shields.io/endpoint?url=https%3A%2F%2Fworkerb.linearb.io%2Fv2%2Fbadge%2Fprivate%2FU2FsdGVkX1992iLVivcpoDwVtCrlTUPBIEmtU4nlPs%2Fcollaboration.svg%3FcacheSeconds%3D60)](https://workerb.linearb.io/v2/badge/collaboration-page?magicLinkId=mFGYFxI)
## Description
- Adding additional specs for space distribution and section deletion.
- Adding unit tests for anvil pasting.


Fixes #33739
_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.Anvil"

### 🔍 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/9443154667>
> Commit: 4a770670aaf2f00d175066b597680345d840cd60
> Cypress dashboard url: <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=9443154667&attempt=1"
target="_blank">Click here!</a>

<!-- 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

- **New Features**
- Added new test cases for verifying section removal and visual aspects
of background-less zones in the Anvil layout system.
- Introduced methods to handle mouse events for space distribution
within sections.
- Added mock data generation functionality for widgets, sections, zones,
and layouts.

- **Tests**
- Implemented tests for paste operations in the Anvil layout system,
including various mock functions and scenarios.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Ashok Kumar M 2024-06-10 16:37:50 +05:30 committed by GitHub
parent 27e772546d
commit 99fa93d61e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 453 additions and 11 deletions

View File

@ -1,4 +1,8 @@
import { agHelper, anvilLayout } from "../../../../support/Objects/ObjectsCore";
import {
agHelper,
anvilLayout,
propPane,
} from "../../../../support/Objects/ObjectsCore";
import { ANVIL_EDITOR_TEST } from "../../../../support/Constants";
import { featureFlagIntercept } from "../../../../support/Objects/FeatureFlags";
import { anvilLocators } from "../../../../support/Pages/Anvil/Locators";
@ -78,5 +82,20 @@ describe(
anvilLayout.verifyWidgetDoesNotExist("Section1");
anvilLayout.sections.verifyZoneCount("Section2", 2);
});
it("3. Verify removing a section through the property pane of a section", () => {
// create a new section with a Zone widget
anvilLayout.dnd.DragDropNewAnvilWidgetNVerify(
anvilLocators.ZONE,
10,
10,
{
skipWidgetSearch: true,
},
);
// delete the section via the property pane
propPane.DeleteWidgetFromPropertyPane("Section1");
anvilLayout.verifyWidgetDoesNotExist("Section1");
});
},
);

View File

@ -1,4 +1,8 @@
import { agHelper, anvilLayout } from "../../../../support/Objects/ObjectsCore";
import {
agHelper,
anvilLayout,
propPane,
} from "../../../../support/Objects/ObjectsCore";
import { ANVIL_EDITOR_TEST } from "../../../../support/Constants";
import { featureFlagIntercept } from "../../../../support/Objects/FeatureFlags";
import { anvilLocators } from "../../../../support/Pages/Anvil/Locators";
@ -147,5 +151,51 @@ describe(
anvilLayout.sections.moveDistributionHandle("right", "Section1", 1, 7);
anvilLayout.sections.verifySectionDistribution("Section1", [10, 2]);
});
it("4. Verify visual check for background less zones and resize indicators", () => {
// create a new section with a button widget
anvilLayout.dnd.DragDropNewAnvilWidgetNVerify(
anvilLocators.WDSBUTTON,
10,
10,
{
skipWidgetSearch: true,
},
);
// create a new zone within the section
anvilLayout.dnd.DragDropNewAnvilWidgetNVerify(
anvilLocators.ZONE,
10,
10,
{
skipWidgetSearch: true,
dropTargetDetails: {
name: "Section1",
},
},
);
const zone1Selector = anvilLocators.anvilWidgetNameSelector("Zone1");
anvilLayout.sections.mouseDownSpaceDistributionHandle("Section1", 1);
// outline color of zone while distributing space should be transparent
cy.get(zone1Selector).should(
"have.css",
"outline-color",
"rgba(0, 0, 0, 0)",
);
anvilLayout.sections.mouseUpSpaceDistributionHandle("Section1", 1);
// select zone1
agHelper.GetNClick(zone1Selector);
// go to style tab
propPane.MoveToTab("Style");
// toggle visual separation off on property pane
propPane.TogglePropertyState("Visual Separation", "Off");
anvilLayout.sections.mouseDownSpaceDistributionHandle("Section1", 1);
// outline color of background less zone while distributing space should not be transparent
cy.get(zone1Selector).should(
"not.have.css",
"outline-color",
"rgba(0, 0, 0, 0)",
);
});
},
);

View File

@ -29,6 +29,22 @@ export class AnvilSectionsZonesHelper {
});
}
public mouseDownSpaceDistributionHandle(
sectionName: string,
distributionHandleIndex: number,
) {
const distributionHandleSelector = `${anvilLocators.anvilWidgetNameSelector(sectionName)} ${anvilLocators.anvilSectionDistributionHandle}:nth-child(${distributionHandleIndex})`;
cy.get(distributionHandleSelector).trigger("mousedown");
}
public mouseUpSpaceDistributionHandle(
sectionName: string,
distributionHandleIndex: number,
) {
const distributionHandleSelector = `${anvilLocators.anvilWidgetNameSelector(sectionName)} ${anvilLocators.anvilSectionDistributionHandle}:nth-child(${distributionHandleIndex})`;
cy.get(distributionHandleSelector).trigger("mouseup");
}
public verifyZoneCount(
sectionOrZoneName: string,
zoneCount: number,

View File

@ -3,7 +3,7 @@ import type { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidg
import { all, call, put, select, takeLeading } from "redux-saga/effects";
import { getSelectedWidgetWhenPasting } from "sagas/WidgetOperationUtils";
import { getWidgets } from "sagas/selectors";
import { updateAndSaveAnvilLayout } from "../../utils/anvilChecksUtils";
import { updateAndSaveAnvilLayout } from "../../../utils/anvilChecksUtils";
import { builderURL } from "@appsmith/RouteBuilder";
import { getCurrentPageId } from "selectors/editorSelectors";
import {
@ -18,13 +18,13 @@ import type {
CopiedWidgetData,
PasteDestinationInfo,
PastePayload,
} from "../../utils/paste/types";
} from "../../../utils/paste/types";
import { getCopiedWidgets } from "utils/storage";
import { getDestinedParent } from "layoutSystems/anvil/utils/paste/destinationUtils";
import { pasteWidgetsIntoMainCanvas } from "layoutSystems/anvil/utils/paste/mainCanvasPasteUtils";
import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants";
import WidgetFactory from "WidgetProvider/factory";
import { getIsAnvilLayout } from "../selectors";
import { getIsAnvilLayout } from "../../selectors";
import { widgetHierarchy } from "layoutSystems/anvil/utils/constants";
function* pasteAnvilModalWidgets(
@ -50,7 +50,7 @@ function* pasteAnvilModalWidgets(
return res;
}
function* pasteWidgetSagas() {
export function* pasteWidgetSagas() {
try {
const {
widgets: copiedWidgets,
@ -69,7 +69,6 @@ function* pasteWidgetSagas() {
const selectedWidget: FlattenedWidgetProps =
yield getSelectedWidgetWhenPasting();
if (!selectedWidget) return;
let allWidgets: CanvasWidgetsReduxState = yield select(getWidgets);
@ -117,7 +116,6 @@ function* pasteWidgetSagas() {
widgetIdMap = res.widgetIdMap;
reverseWidgetIdMap = res.reverseWidgetIdMap;
}
if (modalWidgets.length > 0) {
// paste into main canvas
const res: PastePayload = yield call(

View File

@ -0,0 +1,228 @@
import { select } from "redux-saga/effects";
import { expectSaga } from "redux-saga-test-plan";
import { pasteWidgetSagas } from ".";
import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants";
import { getCopiedWidgets } from "utils/storage";
import {
getNextWidgetName,
getSelectedWidgetWhenPasting,
} from "sagas/WidgetOperationUtils";
import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants";
import { getWidgets } from "sagas/selectors";
import { getCurrentPageId } from "selectors/editorSelectors";
import { SelectionRequestType } from "sagas/WidgetSelectUtils";
import { getDataTree } from "selectors/dataTreeSelectors";
import { generateReactKey } from "utils/generators";
import { LayoutComponentTypes } from "layoutSystems/anvil/utils/anvilTypes";
import { registerLayoutComponents } from "layoutSystems/anvil/utils/layouts/layoutUtils";
import { getLayoutSystemType } from "selectors/layoutSystemSelectors";
const cleanAllMocks = () => {
jest.resetModules(); // Reset the module registry
jest.clearAllMocks(); // Clears any mocked calls
jest.restoreAllMocks(); // Restores initial implementations
};
// Mock the getCopiedWidgets function
jest.mock("utils/storage", () => ({
...jest.requireActual("utils/storage"),
getCopiedWidgets: jest.fn(),
}));
// Mock the getSelectedWidgetWhenPasting function
jest.mock("sagas/WidgetOperationUtils", () => ({
...jest.requireActual("sagas/WidgetOperationUtils"),
getNextWidgetName: jest.fn(),
getSelectedWidgetWhenPasting: jest.fn(),
}));
// Mock only getDataTree
jest.mock("selectors/dataTreeSelectors", () => ({
...jest.requireActual("selectors/dataTreeSelectors"),
getDataTree: jest.fn(),
}));
// Mock getLayoutSystemType of layoutSystemSelector
jest.mock("selectors/layoutSystemSelectors", () => ({
...jest.requireActual("selectors/layoutSystemSelectors"),
getLayoutSystemType: jest.fn(),
}));
describe("pasteSagas", () => {
const pageId = "pageId";
beforeAll(() => {
registerLayoutComponents();
});
beforeEach(() => {
cleanAllMocks();
(getLayoutSystemType as jest.Mock<any, any>).mockReturnValue("ANVIL");
});
it("should perform paste operation with all necessary effects", async () => {
// create mock data for copiedWidgets, selectedWidget, allWidgets
const copiedWidgets: any = {
widgets: [
{
list: [
{
widgetId: "widgetId",
widgetName: "widgetName",
type: "type",
},
],
widgetId: "widgetId",
},
],
};
const selectedWidget: any = { widgetId: MAIN_CONTAINER_WIDGET_ID };
const allWidgets: any = {
widget1: { widgetId: "widget1", parentId: MAIN_CONTAINER_WIDGET_ID },
[MAIN_CONTAINER_WIDGET_ID]: {
widgetId: MAIN_CONTAINER_WIDGET_ID,
layout: [
{
layoutId: generateReactKey(),
layoutType: LayoutComponentTypes.ALIGNED_LAYOUT_COLUMN,
layout: [],
},
],
},
};
// mock the return values of the functions
(getCopiedWidgets as jest.Mock<any, any>).mockResolvedValue(copiedWidgets);
(getSelectedWidgetWhenPasting as jest.Mock<any, any>).mockReturnValue(
selectedWidget,
);
(getDataTree as jest.Mock<any, any>).mockReturnValue({
widget1: {
widgetName: "widget1",
},
});
(getNextWidgetName as jest.Mock<any, any>).mockReturnValue(
"widgetNamecopy",
);
// run the saga
const { effects } = await expectSaga(pasteWidgetSagas as any)
.provide([
[select(getWidgets), allWidgets],
[select(getCurrentPageId), pageId],
])
.run();
// check the effects
expect(effects.put).toHaveLength(3);
const updateLayoutPut = effects.put[0].payload.action;
expect(updateLayoutPut.type).toBe(ReduxActionTypes.UPDATE_LAYOUT);
// check if a new widget is added to main canvas based on widget count on update
expect(Object.keys(updateLayoutPut.payload.widgets).length).toBe(
Object.keys(allWidgets).length + 1,
);
const recordRecentlyAddedWidgetPut = effects.put[1].payload.action;
expect(recordRecentlyAddedWidgetPut.type).toBe(
ReduxActionTypes.RECORD_RECENTLY_ADDED_WIDGET,
);
expect(recordRecentlyAddedWidgetPut.payload.length).toEqual(1);
const selectWidgetInitActionPut = effects.put[2].payload.action;
expect(selectWidgetInitActionPut.type).toBe(
ReduxActionTypes.SELECT_WIDGET_INIT,
);
expect(selectWidgetInitActionPut.payload.selectionRequestType).toEqual(
SelectionRequestType.Multiple,
);
expect(selectWidgetInitActionPut.payload.payload.length).toEqual(1);
});
it("should paste copied modals only to Main canvas", async () => {
// Mock copiedWidgets data
const copiedWidgets = {
widgets: [
{
hierarchy: 2,
list: [
{
widgetId: "1",
widgetName: "1",
detachFromLayout: true,
type: "WDS_MODAL_WIDGET",
},
],
widgetId: "1",
},
{
hierarchy: 1,
list: [
{
widgetId: "2",
widgetName: "2",
type: "WDS_INPUT_WIDGET",
},
],
widgetId: "2",
},
],
};
// Mock selectedWidget data
const selectedWidget = { widgetId: "3", hierarchy: "WDS_INPUT_WIDGET" };
// Mock getWidgets selector
const allWidgets: any = {
1: {
widgetId: "1",
detachFromLayout: true,
parentId: MAIN_CONTAINER_WIDGET_ID,
},
2: { widgetId: "2", parentId: MAIN_CONTAINER_WIDGET_ID },
3: { widgetId: "3", parentId: MAIN_CONTAINER_WIDGET_ID },
[MAIN_CONTAINER_WIDGET_ID]: {
widgetId: MAIN_CONTAINER_WIDGET_ID,
children: ["1", "2", "3"],
layout: [
{
layoutId: generateReactKey(),
layoutType: LayoutComponentTypes.ALIGNED_LAYOUT_COLUMN,
layout: [],
},
],
},
};
// Mock the return values of the functions
(getCopiedWidgets as jest.Mock<any, any>).mockResolvedValue(copiedWidgets);
(getSelectedWidgetWhenPasting as jest.Mock<any, any>).mockReturnValue(
selectedWidget,
);
(getDataTree as jest.Mock<any, any>).mockReturnValue({
1: {
widgetName: "1",
},
2: {
widgetName: "2",
},
3: {
widgetName: "3",
},
});
(getNextWidgetName as jest.Mock<any, any>).mockReturnValue(
Math.random() + "widgetNamecopy",
);
// Run the saga
const { effects } = await expectSaga(pasteWidgetSagas as any)
.provide([
[select(getWidgets), allWidgets],
[select(getCurrentPageId), pageId],
])
.run();
// Check the effects
expect(effects.put).toHaveLength(3);
const updateLayoutPut = effects.put[0].payload.action;
expect(updateLayoutPut.type).toBe(ReduxActionTypes.UPDATE_LAYOUT);
// check if a new widget is added to main canvas based on widget count on update
expect(Object.keys(updateLayoutPut.payload.widgets).length).toBe(
Object.keys(allWidgets).length + 2,
);
const allUpdatedWidgets = Object.values(updateLayoutPut.payload.widgets);
const allUpdatedModals = allUpdatedWidgets.filter(
(widget: any) => widget.type === "WDS_MODAL_WIDGET",
);
// Check if all modals are added to Main canvas
expect(
allUpdatedModals.every(
(each: any) => each.parentId === MAIN_CONTAINER_WIDGET_ID,
),
).toBe(true);
});
});

View File

@ -0,0 +1,45 @@
import { FlexLayerAlignment } from "layoutSystems/common/utils/constants";
import { registerLayoutComponents } from "../../layouts/layoutUtils";
import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants";
import { getDestinedParent } from ".";
import { generateMockDataWithSectionAndZone } from "./mockData.helper";
describe("paste destination utils tests", () => {
beforeAll(() => {
registerLayoutComponents();
});
it("should correctly identify the parent hierarchy for a copied widget when no widget is selected", () => {
const { allWidgets, copiedWidgets } = generateMockDataWithSectionAndZone();
const selectedWidget: any = allWidgets[MAIN_CONTAINER_WIDGET_ID];
const generator = getDestinedParent(
allWidgets,
copiedWidgets,
selectedWidget,
);
const result = generator.next().value;
expect(result.parentOrder).toEqual([MAIN_CONTAINER_WIDGET_ID]);
// correctly identifies the parent hierarchy for a single copied widget
expect(result.alignment).toEqual(FlexLayerAlignment.Start);
// target adding to the main canvas layout
expect(result.layoutOrder).toEqual(
allWidgets[MAIN_CONTAINER_WIDGET_ID].layout,
);
// target adding to the main canvas layout in position 1
expect(result.rowIndex).toEqual([1]);
});
it("should correctly identify the parent hierarchy for a copied widget when a widget is selected", () => {
const { allWidgets, copiedWidgets, mockWidgetId } =
generateMockDataWithSectionAndZone();
const selectedWidget = allWidgets[mockWidgetId];
const generator = getDestinedParent(
allWidgets,
copiedWidgets,
selectedWidget,
);
const result = generator.next().value;
// correctly identifies the parent hierarchy for copied widgets
expect(result.parentOrder).toEqual(["widget-mock", "zone-mock"]);
expect(result.alignment).toEqual(FlexLayerAlignment.Start);
expect(result.rowIndex).toEqual([1, 1, 1]);
});
});

View File

@ -1,12 +1,12 @@
import type { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer";
import type { CopiedWidgetData, PasteDestinationInfo } from "./types";
import type { FlattenedWidgetProps } from "WidgetProvider/constants";
import { getWidgetHierarchy } from "./utils";
import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants";
import type { LayoutProps, WidgetLayoutProps } from "../anvilTypes";
import LayoutFactory from "layoutSystems/anvil/layoutComponents/LayoutFactory";
import type BaseLayoutComponent from "layoutSystems/anvil/layoutComponents/BaseLayoutComponent";
import { FlexLayerAlignment } from "layoutSystems/common/utils/constants";
import type { CopiedWidgetData, PasteDestinationInfo } from "../types";
import { getWidgetHierarchy } from "../utils";
import type { LayoutProps, WidgetLayoutProps } from "../../anvilTypes";
export function* getDestinedParent(
allWidgets: CanvasWidgetsReduxState,

View File

@ -0,0 +1,86 @@
import { FlexLayerAlignment } from "layoutSystems/common/utils/constants";
import { LayoutComponentTypes } from "../../anvilTypes";
import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants";
export function generateMockDataWithSectionAndZone() {
const mockWidgetId = "widget-mock";
const mockSectionId = "section-mock";
const mockZoneId = "zone-mock";
const alignedRowWithWidgets = {
layoutType: LayoutComponentTypes.ALIGNED_WIDGET_ROW,
layout: [
{
alignment: FlexLayerAlignment.Start,
widgetId: mockWidgetId,
widgetType: "WDS_BUTTON_WIDGET",
},
],
};
const zoneLayout = {
layoutType: LayoutComponentTypes.ZONE,
layout: [alignedRowWithWidgets],
};
const sectionLayout = {
layoutType: LayoutComponentTypes.SECTION,
layout: [
{
widgetId: mockZoneId,
alignment: FlexLayerAlignment.Start,
widgetType: "ZONE_WIDGET",
},
],
};
const mainCanvasLayout = {
layoutType: LayoutComponentTypes.ALIGNED_LAYOUT_COLUMN,
layout: [
{
layoutType: LayoutComponentTypes.WIDGET_ROW,
layout: [
{
widgetId: mockSectionId,
alignment: FlexLayerAlignment.Start,
widgetType: "SECTION_WIDGET",
},
],
},
],
};
const allWidgets: any = {
[mockWidgetId]: {
widgetId: mockWidgetId,
type: "WDS_BUTTON_WIDGET",
parentId: mockZoneId,
},
[mockZoneId]: {
widgetId: mockZoneId,
type: "ZONE_WIDGET",
parentId: mockSectionId,
layout: [zoneLayout],
},
[mockSectionId]: {
widgetId: mockSectionId,
type: "SECTION_WIDGET",
parentId: MAIN_CONTAINER_WIDGET_ID,
layout: [sectionLayout],
},
[MAIN_CONTAINER_WIDGET_ID]: {
widgetId: MAIN_CONTAINER_WIDGET_ID,
type: "MAIN_CONTAINER",
parentId: "",
layout: [mainCanvasLayout],
},
};
const copiedWidgets: any = [
{
list: [allWidgets[mockWidgetId]],
hierarchy: 4,
},
];
return {
allWidgets,
copiedWidgets,
mockWidgetId,
mockSectionId,
mockZoneId,
};
}