【问题标题】:Access element whose parent is hidden - cypress.io访问其父级隐藏的元素 - cypress.io
【发布时间】:2018-05-13 02:16:07
【问题描述】:

问题如标题所示,即访问其父元素被隐藏的元素。问题是,根据cypress.io docs

如果满足以下条件,则元素被视为隐藏

  • 它的宽度或高度为0。
  • 它的 CSS 属性(或祖先)是可见性:隐藏。
  • 它的 CSS 属性(或祖先)是 display: none。
  • 它的 CSS 属性是 position:fixed,它在屏幕外或被遮住。

但我正在使用的代码要求我单击一个元素,该元素的父元素是隐藏的,而元素本身是可见的

因此,每次我尝试单击该元素时,都会引发错误读取:

CypressError:重试超时:预期 '' 为'可见'

这个元素 '' 是 不可见,因为它的父级 '' 具有 CSS 属性: '显示:无'

我正在使用的元素是dropdown item,它是用pug 编写的。该元素是angular-mdc-web 中定义的组件,它使用mdc-select 作为下拉菜单,mdc-select-item 作为我必须访问的元素(项目)。

类似结构的示例代码:

//pug
mdc-select(placeholder="installation type"
            '[closeOnScroll]'="true")
    mdc-select-item(value="false") ITEM1
    mdc-select-item(value="true") ITEM2

在上面,ITEM1 是我必须访问的元素。我在cypress.io 中这样做如下:

//cypress.io
// click on the dropdown menu to show the dropdown (items)
cy.get("mdc-select").contains("installation type").click();
// try to access ITEM1
cy.get('mdc-select-item').contains("ITEM1").should('be.visible').click();

已尝试使用 {force:true} 强制单击项目,但没有成功。尝试使用父级mdc-select 上的{enter} 按键来选择项目,但同样没有运气:

CypressError: cy.type() 只能在 textarea 或 :text 上调用。您的 主题是:选择...

还尝试使用select command,但这是不可能的,因为赛普拉斯引擎无法将元素识别为select 元素(因为它不是,内部工作方式不同)。它抛出:

CypressError: cy.select() 只能在 .您的 主题是:选择...

问题mdc-select-menumdc-select-item 的父级,在打开下拉项时通过一些内部计算具有display:none 的属性。

此属性被覆盖为display:flex,但这无济于事。

完全没有想法。这适用于Selenium,但不适用于cypress.io。除了转移到其他框架或更改 UI 代码之外,有什么线索可以解决这种情况吗?

【问题讨论】:

    标签: javascript angular pug ui-automation cypress


    【解决方案1】:

    我可以通过在获取元素后调用scrollIntoView 来解决它。见this answer

    【讨论】:

    • 这救了我?
    【解决方案2】:

    一个相关的问题: Cypress 无法找到选项卡元素,因为它的显示样式为:无(即使它在页面上可见)

    我的解决方法: Cypress 可以通过匹配文本并单击来定位选项卡

        cy.get("[data-cy=parent-element]").contains("target text").click();
    

    【讨论】:

      【解决方案3】:

      为了扩展 BTL 的答案,如果有人遇到错误 - Property 'isVisible' does not exist on type 'Chainable<JQuery<HTMLElement>> in Typescript,以下是我在 cypress 的 commands.ts 顶部添加的内容 -

      declare global {
          namespace Cypress {
              interface Chainable {
                  isVisible;
              }
          }
      } 
      

      如果您看到任何带有期望的 chai 断言错误并且不想导入它,则可能会将 expect(isVisible(subject[0])).to.be.true 替换为 assert.True(isVisible(subject[0])); - 如 Josef Biehler 回答中所示。

      【讨论】:

        【解决方案4】:

        为了方便和可重用性,我不得不混合 Richard Matsen 和 Josef Biehler 的答案。

        定义命令

        // Access element whose parent is hidden
        Cypress.Commands.add('isVisible', {
          prevSubject: true
        }, (subject) => {
          const isVisible = (elem) => !!(
            elem.offsetWidth ||
            elem.offsetHeight ||
            elem.getClientRects().length
          )
          expect(isVisible(subject[0])).to.be.true
        })
        

        您现在可以将其从包含中链接

        describe('Testing select options', function() {
          it('checks select option is visible', function() {
        
            const doc = cy.visit('http://localhost:4200')
            cy.get("mdc-select").contains("installation type").click()
        
            //cy.get('mdc-select-item').contains("ITEM1").should('be.visible') // this will fail
            cy.get('mdc-select-item').contains("ITEM1").isVisible()
          });
        });
        

        【讨论】:

        • 我在尝试使用它时收到Property 'isVisible' does not exist on type 'Chainable<JQuery<HTMLElement>>' 错误。不知何故,命令不可用。
        【解决方案5】:

        我遇到了这个主题,但无法运行您的示例。所以我尝试了一下,我的最终解决方案是这样的。也许其他人也需要这个。请注意,我使用的是打字稿。

        首先:定义自定义命令

        Cypress.Commands.add("isVisible", { prevSubject: true}, (p1: string) => {
              cy.get(p1).should((jq: JQuery<HTMLElement>) => {
                if (!jq || jq.length === 0) {
                    //assert.fail(); seems that we must not assetr.fail() otherwise cypress will exit immediately
                    return;
                }
        
                const elem: HTMLElement = jq[0];
                const doc: HTMLElement = document.documentElement;
                const pageLeft: number = (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0);
                const pageTop: number = (window.pageYOffset || doc.scrollTop)  - (doc.clientTop || 0);
                let elementLeft: number;
                let elementTop: number;
                let elementHeight: number;
                let elementWidth: number;
        
                const length: number = elem.getClientRects().length;
        
                if (length > 0) {
                    // TODO: select correct border box!!
                    elementLeft = elem.getClientRects()[length - 1].left;
                    elementTop = elem.getClientRects()[length - 1].top;
                    elementWidth = elem.getClientRects()[length - 1].width;
                    elementHeight = elem.getClientRects()[length - 1].height;
                }
        
                const val: boolean = !!( 
                    elementHeight > 0 && 
                    elementWidth > 0 && 
                    elem.getClientRects().length > 0 &&
                    elementLeft >= pageLeft &&
                    elementLeft <= window.outerWidth &&
                    elementTop >= pageTop &&
                    elementTop <= window.outerHeight
                );
        
                assert.isTrue(val);
              });
        });
        

        请注意 TODO。就我而言,我的目标是一个有两个边框的按钮。第一个高度和宽度为 0。所以我必须选择第二个。请根据您的需要进行调整。

        第二:使用它

        cy.wrap("#some_id_or_other_locator").isVisible();
        

        【讨论】:

          【解决方案6】:

          在多次咬牙切齿之后,我想我有一个答案。

          我认为根本原因是mdc-select-itemdisplay:flex,这使得它超出了它的父母的界限(严格来说,这感觉就像 display flex 的错误应用,如果我没记错教程的话,但是...)。

          赛普拉斯在确定可见性时会进行大量父项检查,请参阅visibility.coffee

          ## WARNING:
          ## developer beware. visibility is a sink hole
          ## that leads to sheer madness. you should
          ## avoid this file before its too late.
          ...
          when $parent = parentHasDisplayNone($el.parent())
            parentNode = $elements.stringify($parent, "short")
          
            "This element '#{node}' is not visible because its parent '#{parentNode}' has CSS property: 'display: none'"
          ...
          when $parent = parentHasNoOffsetWidthOrHeightAndOverflowHidden($el.parent())
            parentNode  = $elements.stringify($parent, "short")
            width       = elOffsetWidth($parent)
            height      = elOffsetHeight($parent)
          
            "This element '#{node}' is not visible because its parent '#{parentNode}' has CSS property: 'overflow: hidden' and an effective width and height of: '#{width} x #{height}' pixels."
          

          但是,当使用.should('be.visible') 时,我们会遇到父属性未能通过子可见性检查的问题,即使我们实际上可以看到子属性。
          我们需要一个替代测试。

          解决方法

          参考jquery.js,这是元素本身可见性的一种定义(忽略父属性)。

          jQuery.expr.pseudos.visible = function( elem ) {
            return !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length );
          }
          

          因此我们可以将其用作替代方案的基础。

          describe('Testing select options', function() {
          
            // Change this function if other criteria are required.
            const isVisible = (elem) => !!( 
              elem.offsetWidth || 
              elem.offsetHeight || 
              elem.getClientRects().length 
            )
          
            it('checks select option is visible', function() {
          
              const doc = cy.visit('http://localhost:4200')
              cy.get("mdc-select").contains("installation type").click()
          
              //cy.get('mdc-select-item').contains("ITEM1").should('be.visible') //this will fail
              cy.get('mdc-select-item').contains("ITEM1").then (item1 => {
                expect(isVisible(item1[0])).to.be.true
              });
            });
          
            it('checks select option is not visible', function() {
          
              const doc = cy.visit('http://localhost:4200')
              cy.get("mdc-select").contains("installation type").click()
          
              cy.document().then(function(document) {
          
                const item1 = document.querySelectorAll('mdc-select-item')[0]
                item1.style.display = 'none'
          
                cy.get('mdc-select-item').contains("ITEM1").then (item => {
                  expect(isVisible(item[0])).to.be.false
                })
              })
            });
          
            it('checks select option is clickable', function() {
          
              const doc = cy.visit('http://localhost:4200')
              cy.get("mdc-select").contains("installation type").click()
              
              //cy.get('mdc-select-item').contains("ITEM1").click()    // this will fail
              cy.get('mdc-select-item').contains("ITEM1").then (item1 => {
              
                cy.get('mdc-select-item').contains("ITEM2").then (item2 => {
                  expect(isVisible(item2[0])).to.be.true  //visible when list is first dropped
                });
                    
                item1.click();
                cy.wait(500)
                    
                cy.get('mdc-select-item').contains("ITEM2").then (item2 => {
                  expect(isVisible(item2[0])).to.be.false  // not visible after item1 selected
                });
              });
              
            })
          

          脚注 - 使用“then”(或“each”)

          您通常在 cypress 中使用断言的方式是通过命令链,它基本上包装了被测试的元素并处理诸如重试和等待 DOM 更改之类的事情。

          但是,在这种情况下,我们在标准可见性断言 .should('be.visible') 和用于构建页面的框架之间存在矛盾,因此我们使用 then(fn) (ref) 来访问未包装的 DOM。然后,我们可以使用stand jasmine expect 语法应用我们自己版本的可见性测试。

          原来你也可以使用.should(fn)的函数,这个也可以

          it('checks select option is visible - 2', function() {
            const doc = cy.visit('http://localhost:4200')
            cy.get("mdc-select").contains("installation type").click()
          
            cy.get('mdc-select-item').contains("ITEM1").should(item1 => {
              expect(isVisible(item1[0])).to.be.true
            });
          });
          

          使用should 代替then 对可见性测试没有影响,但请注意should 版本可以多次重试该功能,因此不能与click 测试一起使用(例如) .

          来自文档,

          .then() 和 .should()/.and() 有什么区别?

          使用 .then() 仅允许您在回调函数中使用产生的主题,并且应该在您需要操作某些值或执行某些操作时使用。

          另一方面,当使用带有 .should() 或 .and() 的回调函数时,有特殊的逻辑可以重新运行回调函数,直到其中没有任何断言抛出。您应该注意不希望多次执行的 .should() 或 .and() 回调函数中的副作用。

          您也可以通过扩展 chai 断言来解决该问题,但是这方面的文档并不广泛,因此可能需要更多的工作。

          【讨论】:

          • 感谢您为解决问题所付出的所有努力。有用。简单地使用 then 和箭头函数来访问元素就可以解决问题(尽管根据赛普拉斯它仍然是不可见的)。如果可能的话,您能否进一步澄清一下那里的怪异工作原理以及为什么只使用then 它是有效的?
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2012-06-03
          • 1970-01-01
          • 1970-01-01
          • 2016-08-27
          • 1970-01-01
          • 2011-07-15
          • 1970-01-01
          相关资源
          最近更新 更多