Элемент доступа, родительский элемент которого скрыт - cypress.io

Вопрос такой, как указано в заголовке, т. Е. Получить доступ к элементу, родительский элемент которого скрыт. Проблема в том, что согласно документам cypress.io:

Элемент считается скрытым, если:

  • Его ширина или высота равна 0.
  • Его свойство CSS (или предки) - visibility: hidden.
  • Его свойство CSS (или предки) display: none.
  • Его свойство CSS - position: fixed, оно находится вне экрана или скрыто.

Но код, с которым я работаю, требует, чтобы я щелкнул элемент, родительский элемент скрыт, а сам элемент виден.

Поэтому каждый раз, когда я пытаюсь щелкнуть элемент, появляется сообщение об ошибке:

CypressError: Превышено время ожидания повторной попытки: ожидается, что '‹mdc-select-item # mdc-select-item-4.mdc-list-item>' будет 'видимым'

Этот элемент '‹mdc-select-item # mdc-select-item-4.mdc-list-item>' не отображается, потому что его родительский элемент '‹ mdc-select-menu.mdc-simple-menu.mdc-select__menu>' имеет свойство CSS: 'display: none'

введите описание изображения здесь

Я работаю с элементом 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} принудительно щелкнуть элемент, но безуспешно. Пытался выбрать элементы, используя {enter} нажатие клавиши на родительском mdc-select, но снова не повезло, так как он выдает:

CypressError: cy.type () может вызываться только в textarea или: text. Ваша тема: ‹mdc-select-label class =" mdc-select__selected-text "> Выбрать ...‹ / mdc-select-label>

Также пробовал использовать select команду, но это невозможно, потому что движок Cypress не может идентифицировать элемент как select элемент (поскольку это не так, внутренняя работа отличается). Выбрасывает:

CypressError: cy.select () может быть вызван только для. Ваша тема: ‹mdc-select-label class =" mdc-select__selected-text "> Выбрать ...‹ / mdc-select-label>

Проблема заключается в том, что mdc-select-menu, который является родительским для mdc-select-item, имеет свойство display:none в результате некоторых внутренних вычислений при открытии раскрывающихся элементов.

введите описание изображения здесь

Это свойство заменяется на display:flex, но это не помогает.

введите описание изображения здесь

Все без идей. Это работает в Selenium, но не в cypress.io. Любая подсказка, что может быть возможным взломом для ситуации, кроме перехода на другие фреймворки или изменения кода пользовательского интерфейса?


person Kaushik NP    schedule 29.11.2017    source источник


Ответы (6)


После долгого скрежета зубов, думаю, у меня есть ответ.

Я думаю, что основная причина в том, что mdc-select-item имеет display:flex, что позволяет ему выходить за рамки своих родителей (строго говоря, это похоже на неправильное приложение display flex, однако, если я правильно помню учебник ...).

Cypress выполняет множество родительских проверок при определении видимости, см. 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
      });
    });
    
  })

Сноска - использование "затем" (или "каждый")

Обычно вы используете assertion в 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 () /. И ()?

Использование .then () просто позволяет вам использовать полученный объект в функции обратного вызова и его следует использовать, когда вам нужно манипулировать некоторыми значениями или выполнять некоторые действия.

С другой стороны, при использовании функции обратного вызова с .should () или .and () существует специальная логика для повторного запуска функции обратного вызова до тех пор, пока в ней не появятся утверждения. Вы должны быть осторожны с побочными эффектами в функции обратного вызова .should () или .and (), которые вы не хотели бы выполнять несколько раз.

Вы также можете решить проблему, расширив утверждения chai, но документация для этого не является обширной, поэтому потенциально это больше работы.

person Richard Matsen    schedule 04.12.2017
comment
Спасибо за все усилия, приложенные для поиска решения. Оно работает. Простое использование then со стрелкой для доступа к элементу делает свое дело (хотя, по мнению Cypress, он все еще невидим). Не могли бы вы, если возможно, пояснить немного более странной работы там и почему при использовании только then он работает? - person Kaushik NP; 05.12.2017

Для удобства и повторного использования мне пришлось смешать ответ Ричарда Матсена и Йозефа Билера.

Определите команду

// 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()
  });
});
person BTL    schedule 13.08.2019
comment
Я получаю Property 'isVisible' does not exist on type 'Chainable<JQuery<HTMLElement>>' ошибку, когда пытаюсь его использовать. Почему-то команда недоступна. - person infinite; 14.11.2019

Я наткнулся на эту тему, но не смог запустить ваш пример. Итак, я немного попробовал, и мое окончательное решение таково. может кому-то это тоже нужно. Обратите внимание, что я использую машинописный текст.

Во-первых: определите персонализированную команду

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();
person Josef Biehler    schedule 23.01.2019

Связанная проблема: Cypress не смог найти элемент вкладки, потому что у него был стиль отображения: нет (даже если он был виден на странице)

Мое обходное решение: Cypress мог настроить таргетинг на вкладку, сопоставив текст и щелкнув

    cy.get("[data-cy=parent-element]").contains("target text").click();
person Erica Handelman    schedule 17.04.2020

Чтобы немного расширить ответ BTL, если кто-то столкнется с ошибкой - Property 'isVisible' does not exist on type 'Chainable<JQuery<HTMLElement>> в Typescript, следующее - это то, что я добавил вверху commands.ts в кипарисе, чтобы избежать неприятностей -

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

И, возможно, замените expect(isVisible(subject[0])).to.be.true на assert.True(isVisible(subject[0]));, если вы видите какую-либо ошибку утверждения chai с ожиданием и не хотите ее импортировать - как в ответе Йозефа Билера ..

person infinite    schedule 14.11.2019

Я мог бы решить эту проблему, вызвав scrollIntoView после получения элемента. См. этот ответ.

person mrp    schedule 14.01.2021