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>
问题原因分析
- onBlur事件触发过早:第二个下拉框的容器绑定了
onBlur事件,操作输入框后点击Select All按钮时,输入框失去焦点会触发该事件,导致下拉框提前销毁,Select All元素从DOM中移除。 - 元素渲染条件未满足:Select All按钮的渲染依赖
selectedTagType、allowSelectAll、isAllSelected等多个状态,测试执行时可能状态未完全稳定,元素还未渲染完成就被点击。 - 强制点击的局限性:
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
相关产品推荐
相关产品推荐

