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

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

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

  • Его ширина или высота равна 0.
  • Свойство CSS (или его предки) - видимость: скрыто.
  • Свойство 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'

enter image description here

Элементом, с которым я работаю, является dropdown item, который написан на pug. Элемент - это компонент, определенный в angular-mdc-web, который использует mdc-select для выпадающего меню и mdc-select-item для своих элементов (items), к которому я должен получить доступ.

Пример кода аналогичной структуры:

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

Выше ITEM1 - это элемент, к 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"> Select... </mdc-select-label>

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

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

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

enter image description here

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

enter image description here

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

Ответ 1

Я думаю, что после долгих побоев, у меня есть ответ.

Я думаю, что основная причина в том, что у 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
      });
    });

  })

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

Вы обычно используете утверждение в Cypress через цепочки команд, которые в основном оборачивают проверяемые элементы и обрабатывают такие вещи, как повтор и ожидание изменений DOM.

Однако в этом случае мы имеем противоречие между стандартным утверждением видимости .should('be.visible') и структурой, используемой для создания страницы, поэтому мы используем then(fn) (ref), чтобы получить доступ к развернутому DOM. Затем мы можем применить нашу собственную версию теста видимости, используя синтаксис "Жасмин".

Оказывается, вы также можете использовать функцию с .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(), которые не должны выполняться несколько раз.

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

Ответ 2

В документах Синтаксис выбора Cypress, синтаксис

cy.get('mdc-select-item').select('ITEM1')

Вам может понадобиться {force: true}. См. Здесь select_spec.coffee для примеров своих собственных тестов, например

it "can forcibly click even when element is invisible", (done) ->
  select = cy.$$("select:first").hide()
  select.click -> done()
  cy.get("select:first").select("de_dust2", {force: true})

Ответ 3

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

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

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();

Ответ 4

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

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

// 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()
  });
});