You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Cypress中cy.click()失效:元素已脱离DOM?(React+Cypress场景)

Cypress测试React双层下拉框Select All按钮点击失败问题

问题说明

测试React应用的双层下拉框流程:先选择第一个下拉框的选项,再在弹出的第二个下拉框中选择选项,操作完成后下拉框关闭,选中项以Badge形式渲染在DOM中。

测试时第一个下拉框操作正常,但点击第二个下拉框的Select All按钮时,Cypress提示元素已脱离DOM;尝试cy.wait()和.click({force:true})均无效——点击动作虽执行,但组件状态未更新,Badge也未渲染。


相关代码

Cypress测试代码

cy.url().should('include', url);

cy.get("#tag-selector-dropdown").click();
cy.get('#tag_type_option_0').invoke('text').then((inputText) => {
    cy.get('#input-tag-type-dropdown').type(inputText);
    cy.get("[id^='tag_type_option_']").then($options => {
        expect($options).to.have.length(1);
        expect($options.first().text()).to.equal(inputText);
    });
    cy.get('#tag_type_option_0').click();
});
cy.get('#dropdown-tags').should('be.visible')
cy.get('#tag_option_1').invoke('text').then((inputText) => {
    cy.get('#input-tag-dropdown').type(inputText);
    cy.get("[id^='tag_option_']").then($options => {
        expect($options).to.have.length(2);
        expect($options.eq(1).text()).to.equal(inputText);
    });  
    cy.get('#input-tag-dropdown').clear();
});
cy.get('div#tag_option_all').click(); // 此处Cypress无法点击

cy.get("[id^='badge_']").should('be.visible');

Tag Selector组件(双层下拉框+Badge容器)

<TagSelectorDropdown
  id="tag-selector-dropdown"
  filterName="Tags"
  allowSelectAll={allowSelectAll}
  allowNewTag={allowNewTag}
  tags={tags}
  marginLeft={marginLeft ?? 20}
  onAdd={handleTagAdd}
  padding={'3px 10px 3px 10px'}
  width={150}
  containerWidth={250}
  font={14}
  lineHeight={22}
  backgroundColor={backgroundColor}
/>
<TagBadgeContainer tags={tags} onRemoveTag={handleTagRemove} minHeight={minHeight} />

第一个下拉框组件

<SelectDropDown
  id="dropdown-tag-types"
  isVisible={isTagTypeVisible}
  width={width}
  backgroundColor={backgroundColor}
>
  <Scrollbars hideTracksWhenNotNeeded={true} autoHeightMax={150} autoHeight={true}>
    <div>
      <InputWrapper>
        <StyledFaSearch />
        <Input
          id="input-tag-type-dropdown"
          type="text"
          placeholder={'Select Type'}
          value={tagTypeFilter}
          onClick={(e) => e.stopPropagation()}
          onChange={(event) => {
            setTagTypeFilter(event.target.value);
          }}
          width={width}
          font={font}
          backgroundColor={backgroundColor}
          autoFocus
        />
      </InputWrapper>
      {(tagTypesWithTags ?? [])
        .filter((tagType) =>
          tagType.name.toLowerCase().includes(tagTypeFilter.toLowerCase())
        )
        .map((tagType, i) => (
          <SelectOption
            key={tagType.name}
            checked={false}
            value={tagType.name}
            label={tagType.title ?? tagType.name}
            onClick={() => {
              handleSelectTagType(tagType);
            }}
            id={'tag_type_option_' + i.toString()}
          />
        ))}
    </div>
  </Scrollbars>
</SelectDropDown>

第二个下拉框组件

<SelectDropDown
  id="dropdown-tags"
  isVisible={isTagVisible}
  width={width}
  backgroundColor={backgroundColor}
>
  <Scrollbars hideTracksWhenNotNeeded={true} autoHeightMax={150} autoHeight={true}>
    <div
      onBlur={(e) => {
        if (
          document.activeElement === e.currentTarget.querySelector('input[type="text"]')
        ) {
          return;
        }
        setSelectedTagType(undefined);
        setTagVisible(false);
        setTagFilter('');
      }}
    >
      <InputWrapper>
        <StyledFaSearch />
        <Input
          id="input-tag-dropdown"
          type="text"
          placeholder={'Select Tag'}
          value={tagFilter}
          onClick={(e) => e.stopPropagation()}
          onChange={(event) => {
            setTagFilter(event.target.value);
          }}
          width={width}
          font={font}
          autoFocus
        />
      </InputWrapper>
      {selectedTagType &&
        allowSelectAll &&
        isAllSelected(selectedTagType) &&
        allTags.some(
          (tag) =>
            tag.tagTypeName === selectedTagType?.name &&
            !tags.some((selectedTag) => selectedTag.name === tag.name)
        ) && (
          <SelectOption
            key="all"
            checked={false}
            value="all"
            label="Select All"
            onClick={() => {
              handleSelectAll();
            }}
            id="tag_option_all"
          />
        )}
      {allTags
        .filter(
          (tag) =>
            tag.name.toLowerCase().includes(tagFilter.toLowerCase()) &&
            tag.tagTypeName === selectedTagType?.name &&
            !tags.some((selectedTag) => selectedTag.name === tag.name)
        )
        .map((tag, i) => (
          <SelectOption
            key={tag.name}
            checked={false}
            value={tag.name}
            label={tag.title ?? tag.name}
            onClick={() => {
              handleSelectTag(tag);
            }}
            id={'tag_option_' + i.toString()}
          />
        ))}
      {allTags.filter(
        (tag) =>
          tag.name.toLowerCase().includes(tagFilter.toLowerCase()) &&
          tag.tagTypeName === selectedTagType?.name &&
          !tags.some((selectedTag) => selectedTag.name === tag.name)
      ).length === 0 &&
        allowNewTag && (
          <SelectOption
            key="new"
            checked={false}
            value="new"
            isHighlighted={true}
            label="Add new tag"
            onClick={() => {
              setTagModalOpen(true);
            }}
            id="option_new"
          />
        )}
    </div>
  </Scrollbars>
</SelectDropDown>

问题原因分析

  1. onBlur事件触发过早:第二个下拉框的容器绑定了onBlur事件,操作输入框后点击Select All按钮时,输入框失去焦点会触发该事件,导致下拉框提前销毁,Select All元素从DOM中移除。
  2. 元素渲染条件未满足:Select All按钮的渲染依赖selectedTagType、allowSelectAll、isAllSelected等多个状态,测试执行时可能状态未完全稳定,元素还未渲染完成就被点击。
  3. 强制点击的局限性:click({force:true})会跳过元素可见性检查,但如果元素已经处于销毁过程中,点击动作无法触发组件的handleSelectAll方法。

解决方案

方案1:确保元素稳定后再点击

在点击Select All前,明确等待元素可见且状态稳定,避免onBlur提前触发:

// 替换原点击代码
cy.get('#dropdown-tags').should('be.visible');
// 等待Select All按钮完全渲染并可见
cy.get('div#tag_option_all')
  .should('be.visible')
  .click();

方案2:修复组件onBlur逻辑

修改第二个下拉框容器的onBlur事件,判断点击目标是否属于下拉框内部,避免点击选项时关闭下拉框:

<div
  onBlur={(e) => {
    // 检查失去焦点后,焦点是否仍在下拉框内部
    const isFocusInside = e.currentTarget.contains(e.relatedTarget);
    if (isFocusInside || document.activeElement === e.currentTarget.querySelector('input[type="text"]')) {
      return;
    }
    setSelectedTagType(undefined);
    setTagVisible(false);
    setTagFilter('');
  }}
>

方案3:等待数据加载完成

如果标签数据是通过API获取的,在选择第一个下拉框选项后,等待数据加载完成再操作第二个下拉框:

cy.get('#tag_type_option_0').click();
// 拦截标签数据请求,等待请求完成
cy.intercept('GET', '/api/tags/**').as('loadTags');
cy.wait('@loadTags');

cy.get('#dropdown-tags').should('be.visible');
cy.get('div#tag_option_all').click();

方案4:直接调用组件方法

如果DOM操作始终有问题,可以通过Cypress直接调用组件的handleSelectAll方法,绕过DOM交互:

cy.get('#tag-selector-dropdown')
  .then($el => {
    // 获取组件实例(需根据React版本调整获取方式)
    const component = $el[0]._reactRootContainer._internalRoot.current.child.stateNode;
    component.handleSelectAll();
  });
// 等待Badge渲染
cy.get("[id^='badge_']").should('be.visible');

内容的提问来源于stack exchange,提问作者Marko Marchisio

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 15:12:09