-
Notifications
You must be signed in to change notification settings - Fork 4.5k
test: custom widget regression tests #39101
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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); | ||
| }; | ||
|
|
||
| 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"; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"); | ||
| }); | ||
| }); | ||
| }); | ||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Custom Widget "Custom2" Configuration:
|
||
| "minDynamicHeight": 4 | ||
| } | ||
| ] | ||
| } | ||
| } | ||
| 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. |
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
|
||||||||
| } | ||||||||
There was a problem hiding this comment.
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.