为什么Cypress在执行get命令后就说我的元素已分离?

52

目标:我想使用Cypress中的无障碍选择器点击页面上特定的元素。

代码:

cy.findAllByRole('rowheader').eq(2).click();

错误

Timed out retrying: cy.click() failed because this element is detached from the DOM.

<th scope="row" data-automation-id="taskItem" aria-invalid="false" tabindex="-1" class="css-5xw9jq">...</th>

Cypress requires elements be attached in the DOM to interact with them.

The previous command that ran was:

  > cy.eq()

This DOM element likely became detached somewhere between the previous and current command.

问题: 从DOM中我可以看到这个元素还在 - 没有逻辑将此元素从DOM中分离, 而eq方法肯定不会这么做。此外,findAllByRow方法显然正在工作,因为它找到了我想要点击的正确th元素。为什么它说元素被分离了?有没有解决这种情况的方法?


正如你所说,findAllByRole().eq(2)之间不可能出现元素分离的情况,因此问题必定出现在这之前的某个命令上。你能展示完整的测试吗? - user12697177
2
顺便问一下,这是一个React应用程序吗?React经常重新渲染,这是导致元素分离的常见原因。 - user12697177
这是一个React应用程序,但是如何使用Cypress查找元素会导致重新渲染?在我的测试中此时没有发生任何其他异步操作... - Gwynn
请查看此处:https://github.com/cypress-io/cypress/issues/7306#issuecomment-997271455 - user3025289
12个回答

33

这可能不是最好的建议,但您可以尝试以下方法:

cy.findAllByRole('rowheader').eq(2).click({force: true})

14
{force: true} 是唯一对我起作用的东西。 - Grant Birchmeier
这是唯一为我工作的东西。我正在Rails中使用Hotwire,我不知道为什么这么痛苦:( - alvincrespo

27

没有可重现的示例,这只是推测,但请尝试在点击之前使用Cypress.dom.isDetached添加保护。

cy.findAllByRole('rowheader').eq(2)
  .should($el => {
    expect(Cypress.dom.isDetached($el)).to.eq(false)
  })   
  .click()

如果expect失败,则先前的一行导致分离并且cy.findAllByRole没有正确重新查询元素。

如果是这样,您可以尝试使用普通的cy.get()来替换它。


我也喜欢@AntonyFuentesArtavia的想法,因为别名机制保存了原始查询,并在发现其主题被分离时特别重新查询DOM。

请参见我的答案此处

来自Cypress源代码

const resolveAlias = () => {
  // if this is a DOM element
  if ($dom.isElement(subject)) {
    let replayFrom = false

    const replay = () => {
      cy.replayCommandsFrom(command)

      // its important to return undefined
      // here else we trick cypress into thinking
      // we have a promise violation
      return undefined
    }

    // if we're missing any element
    // within our subject then filter out
    // anything not currently in the DOM
    if ($dom.isDetached(subject)) {
      subject = subject.filter((index, el) => $dom.isAttached(el))

      // if we have nothing left
      // just go replay the commands
      if (!subject.length) {
        return replay()
      }
    }

13
不要试图强制点击/操作,而是使用Cypress别名并充分利用内置的断言重试能力。
示例:
cy.get('some locator').first().find('another locator').eq(1).as('element');
cy.get('@element').should(***some assertion to be retried until met***);

尽管我正在遍历许多元素(可能会导致问题,因为其中一个父级可能会被分离),但最终当我在其上加上别名时,我为链的末尾产生的最终元素添加了直接链接。然后,当我引用该别名时,Cypress将重新查询最终元素,并根据添加在其上的任何断言进行必要的重试。这确实帮助我很多,以防止那种分离的问题。

即使在重试期间DOM被重新创建,这仍然有效吗? - Ε Г И І И О
@ΕГИІИО 对我来说运行得很顺畅,你也试试吧! - Antony Fuentes
对我来说,当页面异步刷新时,它(别名)只是变成未定义。唯一的重试方式是不要链接命令。 - Ε Г И І И О

12

我遇到了同样的问题,在运行时出现了相同的错误:

cy.get('<elementId>').should('be.visible').click();

测试运行过程中,我看到它找到了元素(并将其高亮显示),验证了断言,但是某种方式下.click()无法找到该元素,即使它已被链接。

我发现,在此行代码之前添加一个静态等待几秒钟可以解决问题,但我不确定为什么需要这样做,也不想使用静态等待。

没有正在运行的异步任务,因此无法进行动态等待。


1
你知道你的组件是否有多个渲染周期吗?我所想到的唯一可能是,在第一个渲染周期中,get() 和 should('be.visible') 被验证,而在此期间第二个渲染周期开始,因此元素被替换,当 .click() 运行时就会失败。这可以解释为什么静态等待可以解决问题。 - Gwynn
1
Chris,你能添加一下你用来添加静态等待的代码吗? - KyleMit

11
你的问题的答案已经写在了你收到的错误信息中:

重试超时: cy.click() 失败,因为此元素从 DOM 中分离。

...

Cypress 要求与它们交互的元素附加在 DOM 中。

上一个运行的命令是:

cy.eq()

这个 DOM 元素很可能在前一个和当前命令之间变得分离了。

得到这个错误意味着你试图与一个“死”DOM元素交互 - 这意味着它已经被分离或完全从DOM中删除。因此,在cypress即将单击该元素时,eq()要么被分离,要么被从DOM中删除。
在现代JavaScript框架中,DOM元素经常重新渲染 - 这意味着旧元素被丢弃,新元素被放置在其位置。由于这发生得如此之快,用户可能看不到任何可见的变化。但是,如果您正在执行测试命令,则可能您正在交互的元素已变为“死亡”。为了处理这种情况,您必须:
  • 了解应用程序何时重新渲染
  • 重新查询新添加的DOM元素
  • 保护Cypress在满足特定条件之前不运行命令
当我们说保护时,通常意味着:
  • 编写断言
  • 等待XHR
你可以从cypress docsofficial cypress blog中了解更多。
解决方案:确保您的元素首先已加载并可见,然后执行click()
cy.findAllByRole('rowheader').eq(2).should('be.visible').click();

8
两个 Cypress 命令之间如何会出现竞争条件呢?它们是按顺序运行的。 - user15544695
这个命令如何避免竞态问题,而问题中的 cy.findAllByRole('rowheader').eq(2).click(); 方法则不会呢?在任一情况下,在 .click() 之前都可能出现分离的可能性吗?should() 有什么神奇之处可以消除这种可能性吗? - Grant Birchmeier
3
这是一个好问题,我会尝试回答。should('be.visble')或者任何一个should断言都会重复尝试直到条件被满足。在我看来,Cypress分离错误是一个时间游戏,断言可以让我们有更多的时间去稳定DOM,以便下一个Cypress命令可以成功运行。对于我的工作,我已经看到多个断言也能很好地工作,例如cy.get(’selector’.)should('be.visble’).and(‘have.text’, ’some text’)。您还可以通过传递选项来添加超时。除了使用cy.wait之外的任何东西都是一个好方法。 - Alapan Das
1
但是正如你提到的,这并不能保证我们不会出现DOM分离错误。我想这更多是尝试不同方法,看看哪种方法有效。关于此问题,我参考了这篇博客文章https://glebbahmutov.com/blog/detached/。我写下这个答案已经有5个月了,很多事情已经改变了。Cypress引入了“测试重试”,这可能是解决此问题的一种好方法。还有像https://www.npmjs.com/package/cypress-wait-until这样的插件也很好用。此外,很多其他人也写了关于他们的案例中有效的方法,我们也可以去看看。 - Alapan Das
5
守卫也不起作用,例如:cy.get('button').should('be.visible').click().click() 调用时失败。Cypress 确认按钮可见,但在点击时失败。现在告诉我这不是产品中的错误。 - papaiatis

5
这是因为React会重新渲染整个页面。尝试找到带有one command的元素:
// do this
cy.get('[role="rowheader"]:nth-ckild(2)').click();

// instead of this
cy.findAllByRole('rowheader').eq(2).should('be.visible').click();

使用一个命令可以解决在should命令之前Cypress只重试最后一个命令的问题。因此,以前只有.eq(2)会被重试。而你需要cy.findAllByRole('rowheader')也要被重试。
Cypress建议另一种交替使用命令和断言的解决方案。这对我没有用,但你可以尝试一下:
cy.findAllByRole('rowheader').should('be.visible').eq(2).should('be.visible').click();

1

2
虽然这个链接可能回答了问题,但最好在此处包含答案的基本部分并提供参考链接。如果链接页面更改,仅有链接的答案可能会失效。-【来自审查】 - spaleet

0

由于编辑队列已满,无法编辑“Ε Г И І И О”的答案,但我认为为未来的学习者补充答案非常重要。

cy.findAllByRole('rowheader').eq(2).click({force: true})

在点击事件中使用{ force: true }是由于cypress的特性。根据他们的文档,参数force“强制执行操作,禁用等待可操作性”。

至于“等待可操作性”,它引用了断言部分,其中指出:

  • .click()将自动等待元素达到可操作状态
  • .click()将自动重试,直到所有链接的断言都通过为止

因此,这基本上禁用了上述功能,这就是为什么它能够正常工作的原因。在一些管理网站渲染的库/框架(如React和Angular)中,您可能会发现这很有用。


0

我在进行Cypress测试时遇到了与问题标题相同的问题,尝试从下拉列表中点击特定选项。

在测试期间,它会单击此下拉菜单元素,然后尝试从中获取所需选项。

我应用了以下提供的一些断言建议。所有这些都通过了,但是我想要点击的下拉列表中的选项在测试期间没有执行。

  • wait(5000)
  • should($item => { expect(Cypress.dom.isDetached($item)).to.equal(false); })
  • should('be.visible')
  • click({ force: true })

尽管如此,测试仍然给我带来了与之前相同的错误。有人可以在这里提供任何帮助吗?


1
这并没有真正回答问题。如果您有不同的问题,可以通过点击提问来提出。如果您想在此问题获得新的答案时得到通知,您可以关注此问题。一旦您拥有足够的声望,您还可以添加悬赏以吸引更多关注。- 来自审核 - Luca Kiebel

0
我解决这个问题的方法是将文件中的超时时间从4秒增加到10秒。在配置对象的根级别添加"defaultCommandTimeout": 10000
这不是一个修复,而是一个解决方法。如果页面上的元素需要太长时间才能交互,那么就有一些问题需要调试。
此外,请确保您在构建环境下运行测试,而不是开发环境。构建是编译后的代码,也是用户将看到的代码。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接