Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import {
agHelper,
assertHelper,
locators,
propPane,
} from "../../../../../support/Objects/ObjectsCore";

import EditorNavigation, {
EntityType,
} from "../../../../../support/Pages/EditorNavigation";

describe("Custom widget Tests", {}, function () {
before(() => {
agHelper.AddDsl("customWidgetWithSrc");
});

const getIframeBody = () => {
// get the iframe > document > body
// and retry until the body element is not empty
return cy
.get(".t--widget-customwidget iframe")
.last()
.its("0.contentDocument.body")
.should("not.be.empty")
.then(cy.wrap);
Comment on lines +20 to +25
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid using cy.get().last() for iframe selection.

Using .last() to select an iframe is fragile and may break if more iframes are added. Consider using a more specific selector.

-      .get(".t--widget-customwidget iframe")
-      .last()
+      .get(".t--widget-customwidget iframe[data-testid='custom-widget-iframe']")

};

it("should check that custom widget property pane details are coming up properly", () => {
const defaultModel = `{
"tips": [
"Pass data to this widget in the default model field",
"data in the javascript file using the appsmith.model variable",
"Create events in the widget and trigger them in the javascript file using appsmith.triggerEvent('eventName')",
"Access data in CSS as var(--appsmith-model-{property-name})"
]
}`;
const custom2WidgetBoundary =
"//div[@data-widgetname-cy='Custom2']//div[contains(@class,'widget-component-boundary')]//div//div";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Replace XPath with data-testid attribute.

Using XPath selectors is discouraged in Cypress tests. Use data-testid attributes instead.

-      "//div[@data-widgetname-cy='Custom2']//div[contains(@class,'widget-component-boundary')]//div//div";
+      "[data-testid='custom2-widget-boundary']";


EditorNavigation.SelectEntityByName("Custom2", EntityType.Widget);
getIframeBody().find(".tip-container").should("exist");
getIframeBody()
.find(".tip-container p")
.should(
"have.text",
"Pass data to this widget in the default model field",
);

agHelper.UpdateCodeInput(propPane._propDefaultModel, defaultModel);

getIframeBody().find("button.primary").trigger("click");

getIframeBody()
.find(".tip-container p")
.should(
"have.text",
"data in the javascript file using the appsmith.model variable",
);

EditorNavigation.SelectEntityByName("Custom1", EntityType.Widget);
propPane.TogglePropertyState("Visible", "Off");
agHelper.GetNClick(locators._previewModeToggle("edit"));
agHelper.AssertElementAbsence(locators._widgetByName("Custom1"));
agHelper.GetNClick(locators._previewModeToggle("preview"));
propPane.TogglePropertyState("Visible", "On");

EditorNavigation.SelectEntityByName("Custom2", EntityType.Widget);

propPane.ToggleJSMode("onResetClick");
propPane.EnterJSContext(
"onResetClick",
`{{showAlert('Reset Clicked', 'error');}}`,
);
getIframeBody().find("button.reset").trigger("click");
agHelper.ValidateToastMessage("Reset Clicked");

propPane.MoveToTab("Style");
// Background color
agHelper.GetNClick(propPane._propertyControlColorPicker("backgroundcolor"));
agHelper.AssertElementVisibility(propPane._colorPickerV2Color);
propPane.ToggleJSMode("backgroundcolor", true);
propPane.UpdatePropertyFieldValue("Background color", "#eab308");
agHelper.AssertCSS(
`(${locators._draggableCustomWidget}//div)[10]`,
"background-color",
"rgb(239, 117, 65)",
);
propPane.ToggleJSMode("backgroundcolor", false);

// Border Color
propPane.SelectColorFromColorPicker("bordercolor", 13);
assertHelper.AssertNetworkStatus("@updateLayout");

agHelper
.GetWidgetCSSFrAttribute(propPane._borderColorCursor, "background-color")
.then((color) => {
agHelper
.GetWidgetCSSFrAttribute(custom2WidgetBoundary, "border-color")
.then((bgcolor) => {
expect(color).to.eq(bgcolor);
});
});

// Verify border width
propPane.UpdatePropertyFieldValue("Border width", "7");
agHelper
.GetWidgetCSSFrAttribute(custom2WidgetBoundary, "border-width")
.then((width) => {
expect(width).to.eq("7px");
});
});
});
149 changes: 149 additions & 0 deletions app/client/cypress/fixtures/customWidgetWithSrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
{
"dsl": {
"widgetName": "MainContainer",
"backgroundColor": "none",
"rightColumn": 4896,
"snapColumns": 64,
"detachFromLayout": true,
"widgetId": "0",
"topRow": 0,
"bottomRow": 730,
"containerStyle": "none",
"snapRows": 124,
"parentRowSpace": 1,
"type": "CANVAS_WIDGET",
"canExtend": true,
"version": 91,
"minHeight": 1292,
"dynamicTriggerPathList": [],
"parentColumnSpace": 1,
"dynamicBindingPathList": [],
"leftColumn": 0,
"children": [
{
"needsErrorInfo": false,
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}",
"mobileBottomRow": 42,
"widgetName": "Custom1",
"borderColor": "#E0DEDE",
"srcDoc": {
"html": " <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/antd/5.11.1/antd.min.css\" />\n <div id=\"root\"></div>",
"css": ".table-container {\n padding: 20px;\n background-color: #f9f9f9;\n border-radius: 8px;\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n}\n\n.table {\n width: 100%;\n border-collapse: collapse;\n}\n\n.table th, .table td {\n padding: 12px;\n border-bottom: 1px solid #ddd;\n text-align: left;\n}\n\n.table th {\n background-color: #f1f1f1;\n font-weight: bold;\n}\n\n.table tr:hover {\n background-color: #f5f5f5;\n}",
"js": "import React from 'https://cdn.jsdelivr.net/npm/react@18.2.0/+esm';\nimport ReactDOM from 'https://cdn.jsdelivr.net/npm/react-dom@18.2.0/+esm';\nfunction TableWidget() {\n // Mock data for the table\n const [data, setData] = React.useState(appsmith.model.data || [{\n name: 'John Doe',\n age: 28,\n occupation: 'Engineer'\n }, {\n name: 'Jane Smith',\n age: 34,\n occupation: 'Designer'\n }, {\n name: 'Sam Green',\n age: 45,\n occupation: 'Manager'\n }]);\n React.useEffect(() => {\n appsmith.onModelChange((model, prevModel) => {\n if (JSON.stringify(model.data) !== JSON.stringify(prevModel?.data)) {\n setData(model.data);\n }\n });\n }, []);\n return /*#__PURE__*/React.createElement(\"div\", {\n className: \"table-container\"\n }, /*#__PURE__*/React.createElement(\"table\", {\n className: \"table\"\n }, /*#__PURE__*/React.createElement(\"thead\", null, /*#__PURE__*/React.createElement(\"tr\", null, /*#__PURE__*/React.createElement(\"th\", null, \"Name\"), /*#__PURE__*/React.createElement(\"th\", null, \"Age\"), /*#__PURE__*/React.createElement(\"th\", null, \"Occupation\"))), /*#__PURE__*/React.createElement(\"tbody\", null, data.map((row, index) => /*#__PURE__*/React.createElement(\"tr\", {\n key: index\n }, /*#__PURE__*/React.createElement(\"td\", null, row.name), /*#__PURE__*/React.createElement(\"td\", null, row.age), /*#__PURE__*/React.createElement(\"td\", null, row.occupation))))));\n}\nappsmith.onReady(() => {\n ReactDOM.render( /*#__PURE__*/React.createElement(TableWidget, null), document.getElementById(\"root\"));\n});"
},
"isCanvas": false,
"topRow": 10,
"bottomRow": 40,
"parentRowSpace": 10,
"type": "CUSTOM_WIDGET",
"mobileRightColumn": 31,
"dynamicTriggerPathList": [
{
"key": "onResetClick"
}
],
"parentColumnSpace": 7.890625,
"dynamicBindingPathList": [
{
"key": "theme"
},
{
"key": "borderRadius"
},
{
"key": "boxShadow"
}
],
"leftColumn": 8,
"defaultModel": "{\n \"tips\": [\n \"Pass data to this widget in the default model field\",\n \"Access data in the javascript file using the appsmith.model variable\",\n \"Create events in the widget and trigger them in the javascript file using appsmith.triggerEvent('eventName')\",\n \"Access data in CSS as var(--appsmith-model-{property-name})\"\n ]\n}",
"borderWidth": "1",
"theme": "{{appsmith.theme}}",
"onResetClick": "{{showAlert('Successfully reset!!', '');}}",
"events": ["onResetClick"],
"key": "podxgd85w9",
"backgroundColor": "#FFFFFF",
"rightColumn": 56,
"dynamicHeight": "FIXED",
"isSearchWildcard": true,
"widgetId": "zkhwsvct6f",
"isVisible": true,
"version": 1,
"uncompiledSrcDoc": {
"html": " <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/antd/5.11.1/antd.min.css\" />\n <div id=\"root\"></div>",
"css": ".table-container {\n padding: 20px;\n background-color: #f9f9f9;\n border-radius: 8px;\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n }\n .table {\n width: 100%;\n border-collapse: collapse;\n }\n .table th, .table td {\n padding: 12px;\n border-bottom: 1px solid #ddd;\n text-align: left;\n }\n .table th {\n background-color: #f1f1f1;\n font-weight: bold;\n }\n .table tr:hover {\n background-color: #f5f5f5;\n }",
"js": " import React from 'https://cdn.jsdelivr.net/npm/react@18.2.0/+esm';\n import ReactDOM from 'https://cdn.jsdelivr.net/npm/react-dom@18.2.0/+esm';\n function TableWidget() {\n // Mock data for the table\n const [data, setData] = React.useState(appsmith.model.data || [\n { name: 'John Doe', age: 28, occupation: 'Engineer' },\n { name: 'Jane Smith', age: 34, occupation: 'Designer' },\n { name: 'Sam Green', age: 45, occupation: 'Manager' }\n ]);\n React.useEffect(() => {\n appsmith.onModelChange((model, prevModel) => {\n if (JSON.stringify(model.data) !== JSON.stringify(prevModel?.data)) {\n setData(model.data);\n }\n });\n }, []);\n return (\n <div className=\"table-container\">\n <table className=\"table\">\n <thead>\n <tr>\n <th>Name</th>\n <th>Age</th>\n <th>Occupation</th>\n </tr>\n </thead>\n <tbody>\n {data.map((row, index) => (\n <tr key={index}>\n <td>{row.name}</td>\n <td>{row.age}</td>\n <td>{row.occupation}</td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n );\n }\n appsmith.onReady(() => {\n ReactDOM.render(<TableWidget />, document.getElementById(\"root\"));\n });"
},
"parentId": "0",
"renderMode": "CANVAS",
"isLoading": false,
"mobileTopRow": 12,
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"mobileLeftColumn": 8,
"maxDynamicHeight": 9000,
"minDynamicHeight": 4
},
{
"needsErrorInfo": false,
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}",
"mobileBottomRow": 75,
"widgetName": "Custom2",
"borderColor": "#E0DEDE",
"srcDoc": {
"html": "<!-- no need to write html, head, body tags, it is handled by the widget -->\n<div id=\"root\"></div>\n",
"css": ".app {\n\twidth: calc(var(--appsmith-ui-width) * 1px);\n\tjustify-content: center;\n\tborder-radius: 0px;\n\tborder: none;\n}\n\n.tip-container {\n margin-bottom: 20px;\n}\n\n.tip-container h2 {\n margin-bottom: 20px;\n\tfont-size: 16px;\n\tfont-weight: 700;\n}\n\n.tip-header {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\talign-items: baseline;\n}\n\n.tip-header div {\n\tcolor: #999;\n}\n\n.button-container {\n\ttext-align: right;\t\n}\n\n.button-container button {\n margin: 0 10px;\n\tborder-radius: var(--appsmith-theme-borderRadius) !important;\n}\n\n.button-container button.primary {\n\tbackground: var(--appsmith-theme-primaryColor) !important;\n}\n\n.button-container button.reset:not([disabled]) {\n\tcolor: var(--appsmith-theme-primaryColor) !important;\n\tborder-color: var(--appsmith-theme-primaryColor) !important;\n}",
"js": "import React from 'https://cdn.jsdelivr.net/npm/react@18.2.0/+esm';\nimport reactDom from 'https://cdn.jsdelivr.net/npm/react-dom@18.2.0/+esm';\nimport { Button, Card } from 'https://cdn.jsdelivr.net/npm/antd@5.11.1/+esm';\nimport Markdown from 'https://cdn.jsdelivr.net/npm/react-markdown@9.0.1/+esm';\n\nfunction App() {\n const [currentIndex, setCurrentIndex] = React.useState(0);\n const handleNext = () => {\n setCurrentIndex(prevIndex => (prevIndex + 1) % appsmith.model.tips.length);\n };\n const handleReset = () => {\n setCurrentIndex(0);\n appsmith.triggerEvent(\"onResetClick\");\n };\n return /*#__PURE__*/React.createElement(Card, {\n className: \"app\",\n }, /*#__PURE__*/React.createElement(\"div\", {\n className: \"tip-container\"\n }, /*#__PURE__*/React.createElement(\"div\", {\n className: \"tip-header\"\n }, /*#__PURE__*/React.createElement(\"h2\", null, \"Custom Widget\"), /*#__PURE__*/React.createElement(\"div\", null, currentIndex + 1, \" / \", appsmith.model.tips.length, \" \")), /*#__PURE__*/React.createElement(Markdown, null, appsmith.model.tips[currentIndex])), /*#__PURE__*/React.createElement(\"div\", {\n className: \"button-container\"\n }, /*#__PURE__*/React.createElement(Button, {\n className: \"primary\",\n onClick: handleNext,\n type: \"primary\"\n }, \"Next Tip\"), /*#__PURE__*/React.createElement(Button, {\n\tclassName: \"reset\",\n\tdisabled: currentIndex === 0,\n onClick: handleReset\n }, \"Reset\")));\n}\nappsmith.onReady(() => {\n reactDom.render( /*#__PURE__*/React.createElement(App, null), document.getElementById(\"root\"));\n});"
},
"isCanvas": false,
"topRow": 43,
"bottomRow": 73,
"parentRowSpace": 10,
"type": "CUSTOM_WIDGET",
"mobileRightColumn": 41,
"dynamicTriggerPathList": [
{
"key": "onResetClick"
}
],
"parentColumnSpace": 14.21875,
"dynamicBindingPathList": [
{
"key": "theme"
},
{
"key": "borderRadius"
},
{
"key": "boxShadow"
}
],
"leftColumn": 19,
"defaultModel": "{\n \"tips\": [\n \"Pass data to this widget in the default model field\",\n \"Access data in the javascript file using the appsmith.model variable\",\n \"Create events in the widget and trigger them in the javascript file using appsmith.triggerEvent('eventName')\",\n \"Access data in CSS as var(--appsmith-model-{property-name})\"\n ]\n}",
"borderWidth": "1",
"theme": "{{appsmith.theme}}",
"onResetClick": "{{showAlert('Successfully reset!!', '');}}",
"events": ["onResetClick"],
"key": "podxgd85w9",
"backgroundColor": "#FFFFFF",
"rightColumn": 42,
"dynamicHeight": "FIXED",
"isSearchWildcard": true,
"widgetId": "9iep04z42x",
"isVisible": true,
"version": 1,
"uncompiledSrcDoc": {
"html": "<!-- no need to write html, head, body tags, it is handled by the widget -->\n<div id=\"root\"></div>\n",
"css": ".app {\n width: calc(1px * var(--appsmith-ui-width));\n justify-content: center;\n border-radius: 0px;\n border: none;\n \n .tip-container {\n margin-bottom: 20px;\n\n h2 {\n margin-bottom: 20px;\n font-size: 16px;\n font-weight: 700;\n }\n\n .tip-header {\n display: flex;\n justify-content: space-between;\n align-items: baseline;\n\n div {\n color: #999;\n }\n }\n }\n\t\n\t.button-container {\n text-align: right;\n\n button {\n margin: 0 10px;\n border-radius: var(--appsmith-theme-borderRadius) !important;\n\n &.primary {\n background: var(--appsmith-theme-primaryColor) !important;\n }\n\n &.reset:not([disabled]) {\n color: var(--appsmith-theme-primaryColor) !important;\n border-color: var(--appsmith-theme-primaryColor) !important;\n }\n }\n }\n}\n",
"js": "import React from 'https://cdn.jsdelivr.net/npm/react@18.2.0/+esm'\nimport reactDom from 'https://cdn.jsdelivr.net/npm/react-dom@18.2.0/+esm'\nimport { Button, Card } from 'https://cdn.jsdelivr.net/npm/antd@5.11.1/+esm'\nimport Markdown from 'https://cdn.jsdelivr.net/npm/react-markdown@9.0.1/+esm';\n\nfunction App() {\n\tconst [currentIndex, setCurrentIndex] = React.useState(0);\n\n\tconst handleNext = () => {\n\t\tsetCurrentIndex((prevIndex) => (prevIndex + 1) % appsmith.model.tips.length);\n\t};\n\n\tconst handleReset = () => {\n\t\tsetCurrentIndex(0);\n\t\tappsmith.triggerEvent(\"onResetClick\");\n\t};\n\n\treturn (\n\t\t<Card className=\"app\">\n\t\t\t<div className=\"tip-container\">\n\t\t\t\t<div className=\"tip-header\">\n\t\t\t\t\t<h2>Custom Widget</h2>\n\t\t\t\t\t<div>{currentIndex + 1} / {appsmith.model.tips.length}\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<Markdown>{appsmith.model.tips[currentIndex]}</Markdown>\n\t\t\t</div>\n\t\t\t<div className=\"button-container\">\n\t\t\t\t<Button className=\"primary\" onClick={handleNext} type=\"primary\">Next Tip</Button>\n\t\t\t\t<Button className=\"reset\" disabled={currentIndex === 0} onClick={handleReset}>Reset</Button>\n\t\t\t</div>\n\t</Card>\n);\n}\n\nappsmith.onReady(() => {\n\t/*\n\t * This handler function will get called when parent application is ready.\n\t * Initialize your component here\n\t * more info - https://docs.appsmith.com/reference/widgets/custom#onready\n\t */\n\treactDom.render(<App />, document.getElementById(\"root\"));\n});"
},
"parentId": "0",
"renderMode": "CANVAS",
"isLoading": false,
"mobileTopRow": 45,
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"mobileLeftColumn": 18,
"maxDynamicHeight": 9000,
Comment on lines +85 to +144
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Custom Widget "Custom2" Configuration:
While the overall structure for "Custom2" is consistent with "Custom1," a few points need attention:

  • The "key" property on line 125 has the same value ("podxgd85w9") as used in "Custom1" (line 63). Unique keys are typically expected to avoid identification or rendering issues.
  • In the "uncompiledSrcDoc" (lines 133-137), the CSS block contains nested selectors. Plain CSS does not allow nesting unless a preprocessor (e.g., Sass) is in use; please verify if this is intentional.
  • Additionally, the import of react-dom is done as "reactDom" (in contrast to "ReactDOM" in "Custom1"); standardizing the naming might help maintain consistency.

"minDynamicHeight": 4
}
]
}
}
4 changes: 2 additions & 2 deletions app/client/cypress/limited-tests.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# To run only limited tests - give the spec names in below format:
#cypress/e2e/Regression/ClientSide/VisualTests/JSEditorIndent_spec.js
cypress/e2e/Regression/ClientSide/Widgets/Custom/CustomWidget_PropertyPane_Validation.ts
# For running all specs - uncomment below:
#cypress/e2e/**/**/*
cypress/e2e/Regression/ClientSide/Anvil/Widgets/*
#cypress/e2e/Regression/ClientSide/Anvil/Widgets/*

#ci-test-limit uses this file to run minimum of specs. Do not run entire suite with this command.
2 changes: 2 additions & 0 deletions app/client/cypress/support/Objects/CommonLocators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,4 +355,6 @@ export class CommonLocators {
_dropdownActiveOption = ".rc-select-dropdown .rc-select-item-option-active";
_homeIcon = "[data-testid='t--default-home-icon']";
_widget = (widgetName: string) => `.t--widget-${widgetName}`;
_draggableCustomWidget =
"//div[contains(@class,'t--draggable-customwidget')]";
Comment on lines +358 to +359
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Replace XPath with data-testid attribute for custom widget selector.

Using XPath selectors is discouraged. Use data-testid attributes for better maintainability.

-  _draggableCustomWidget =
-    "//div[contains(@class,'t--draggable-customwidget')]";
+  _draggableCustomWidget = "[data-testid='draggable-custom-widget']";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
_draggableCustomWidget =
"//div[contains(@class,'t--draggable-customwidget')]";
_draggableCustomWidget = "[data-testid='draggable-custom-widget']";

}
1 change: 1 addition & 0 deletions app/client/cypress/support/Pages/PropertyPane.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ export class PropertyPane {
_buttonWidget = "[data-widgetname-cy='Button1']";
_getActionCardSelector = (type: string) =>
`[data-testid='action-card-Show ${type}']`;
_propDefaultModel = ".t--property-pane-section-defaultmodel";

public OpenJsonFormFieldSettings(fieldName: string) {
this.agHelper.GetNClick(this._jsonFieldEdit(fieldName));
Expand Down
Loading