如何将 cypress 与使用 webpack 预取或预加载的组件一起使用?

How do I use cypress with components that are prefetched or preloaded with webpack?

我正在使用 Cypress 7.7.0(也在 8.0.0 上测试过),我 运行 进入了一个有趣的竞争条件。我正在测试一个页面,其中 Cypress 执行的第一个交互是单击按钮打开模式。为了保持较小的包大小,我将模式拆分为它自己的预取 webpack 块。我的 Cypress 测试以 cy.get('#modal-button').click() 开始,但这不会加载模态,因为模态尚未完成 downloading/loading。它什么都不做(甚至不向控制台抛出任何错误)。换句话说,Cypress 与页面交互的速度太快了。这也通过手动测试重现(我在页面加载后超快地点击了按钮)。我试过将模式设置为预加载,但这也不起作用。

我能够通过在页面加载和按钮交互之间引入更多延迟来解决问题。例如,在我单击按钮之前插入任何 Cypress 命令(甚至是 cy.wait(0))即可修复解决方案。然而,Cypress 以不需要插入这些脆弱的解决方案而闻名。有解决这个问题的好方法吗?我想将模态保留在它自己的块中。

仅供参考:我使用 Vue 作为我的前端库,并使用简单的 defineAsyncComponent(() => import(/* webpackPrefetch: true */ './my-modal.vue')) 来加载模态组件。我认为这个问题对 Cypress 来说是普遍的。

您的问题似乎是您在加载支持按钮的代码之前就已经渲染了按钮。正如您所注意到的,这不仅是快速自动机器人的问题,甚至是“普通”用户的问题。

简而言之,解决方案是不提前显示按钮,而是显示一个加载对话框。赛普拉斯甚至允许 waiting for a DOM element to be visible 超时选项。这比脆弱的随机等待更稳健。

cy.wait(0)没问题。

您所做的只是将控制权从测试传递给 JS 队列中的下一个进程,在本例中,它是应用程序的启动脚本,它可能正在等待将点击处理程序添加到按钮。

我最近发现 React hooks 应用程序也需要这样做,以允许 hook 完成它的过程。您可能还会在 Vue 3 中遇到这种情况,因为它们引入了类似钩子的功能。

如果你想凭经验测试事件处理程序是否已经到达,你可以使用这里给出的方法(为click()修改)- When Can The Test Start?

let appHasStarted

function spyOnAddEventListener (win) {
  const addListener = win.EventTarget.prototype.addEventListener
  win.EventTarget.prototype.addEventListener = function (name) {
    if (name === 'click') {
      appHasStarted = true
      win.EventTarget.prototype.addEventListener = addListener  // restore original listener
    }
    return addListener.apply(this, arguments)
  }
}

function waitForAppStart() {
  return new Cypress.Promise((resolve, reject) => {
    const isReady = () => {
      if (appHasStarted) {
        return resolve()
      }
      setTimeout(isReady, 0)  // recheck "appHasStarted" variable
    }
    isReady()
  })
}

it('greets', () => {
  cy.visit('app.html', {
    onBeforeLoad: spyOnAddEventListener
  }).then(waitForAppStart)

  cy.get('#modal-button').click()
})

但请注意 setTimeout(isReady, 0) 可能只会在您的应用中实现与 cy.wait(0) 相同的效果,即您实际上不需要轮询事件处理程序,您只需要应用采取呼吸。

我最终还是等待网络空闲,尽管我有多种选择。

我用来执行此操作的 cypress 函数如下,它深受 this solution for waiting on the network 的影响:

Cypress.Commands.add('waitForIdleNetwork', () => {
    const idleTimesInit = 3
    let idleTimes = idleTimesInit
    let resourcesLengthPrevious

    cy.window().then(win =>
        cy.waitUntil(() => {
            const resourcesLoaded = win.performance.getEntriesByType('resource')

            if (resourcesLoaded.length === resourcesLengthPrevious) {
                idleTimes--
            } else {
                idleTimes = idleTimesInit
                resourcesLengthPrevious = resourcesLoaded.length
            }

            return !idleTimes
        })
    )
})

以下是我采用的解决方案的优缺点:

  • 优点:当用户可能永远不会 运行 遇到此问题时,无需增加包大小或修改客户端代码
  • 缺点:技术上仍然可能出现竞争条件,点击事件发生在资产下载之后,但在它们全部执行和呈现它们的内容之前,但不太可能,不如等待 UI 本身用于指示何时就绪

这是我选择的解决方法,但以下解决方法也有效:

  • 创建轻量级占位符组件以在异步组件下载时代替异步组件,并让 cypress 等待实际组件呈现(例如,在后台下载实际模态时仅显示微调器的默认模态)
    • 优点:不必等待网络资源,如果实施得当可避免所有竞争条件
    • 缺点:必须创建用户可能永远看不到的组件,增加包大小
  • cy.wait(...)“睡”任意量(虽然这很脆弱)
    • 优点:易于实施
    • 缺点:脆弱,Cypress 不建议直接使用它,如果使用 eslint-plugin-cypress 会导致 linter 问题(您可以在使用它的行上禁用 eslint,但它“感觉很难看”我(不讨厌任何以这种方式编程的人)