Cypress 踩坑記 - DOM 遮擋

嘿嘿不務正業發表於2023-05-16
Cypress 是一個非常流行的測試工具,然而實際使用過程中發現一些問題,這裡做些記錄。

問題發現

Cypressclick 是非常常用的指令,然而在一些特殊場景下 click 並不能如想象中那般正常工作。

比如現在有一個彈窗,我們需要測試在點選遮罩層時是否可以正常關閉彈窗。

picture 1

測試程式碼比較簡單:

/// <reference types="cypress" />

context('Actions', () => {
    beforeEach(() => {
        cy.visit('http://localhost:3300/Modal');
    });

    it('Override', () => {
        cy.get('.mantine-Button-root').click();
        cy.get('.mantine-Modal-root').should('exist');
        cy.get('.mantine-Modal-overlay').click();
    });
});

然後執行 Cypress,發現一切如想象中那般簡單,很順利就透過了。

picture 2

然而當往 Model 中填充了一些內容後,卻發現突然這裡就報錯了。

picture 3

當然,報錯是沒問題,遮罩層確實被內容遮擋了。問題是剛剛明明也是一樣被遮擋,為何就不報錯,只是因為內容多了一點就報錯,這就很不合適了。

檢視文件會發現 click 還支援座標或位置引數。

picture 4

然而,並沒有什麼用,也就是說這個點選位置無關,應該是和 Cypress 判斷元素遮擋有關係,看起來 Cypress 遮擋計算還需要最佳化。

原因排查

排查原始碼可以發現 Cypressclick 會經過一些判定:

if (force !== true) {
    // now that we know our element isn't animating its time
    // to figure out if it's being covered by another element.
    // this calculation is relative from the viewport so we
    // only care about fromElViewport coords
    $elAtCoords =
        options.ensure.notCovered && ensureElIsNotCovered(cy, win, $el, coords.fromElViewport, options, _log, onScroll);
    Cypress.ensure.isNotHiddenByAncestors($el, name, _log);
}

其中比較重要的引數是 coords.fromElViewport,其數值長這樣:

{
    "top": 0,
    "left": 0,
    "right": 1000,
    "bottom": 660,
    "topCenter": 330,
    "leftCenter": 500,
    "x": 500,
    "y": 330
}

注意其中的 xy,可以認為就是中心點的座標。

然後 Cypress 會使用該座標獲取該位置最頂層的元素:

const getElementAtPointFromViewport = function (fromElViewport) {
    // get the element at point from the viewport based
    // on the desired x/y normalized coordinations
    let elAtCoords;

    elAtCoords = $dom.getElementAtPointFromViewport(win.document, fromElViewport.x, fromElViewport.y);

    if (elAtCoords) {
        $elAtCoords = $dom.wrap(elAtCoords);

        return $elAtCoords;
    }

    return null;
};

const ensureDescendents = function (fromElViewport) {
    // figure out the deepest element we are about to interact
    // with at these coordinates
    $elAtCoords = getElementAtPointFromViewport(fromElViewport);
    debug('elAtCoords', $elAtCoords);
    debug('el has pointer-events none?');
    ensureElDoesNotHaveCSS($el, 'pointer-events', 'none', name, log);
    debug('is descendent of elAtCoords?');
    ensureIsDescendent($el, $elAtCoords, name, log);

    return $elAtCoords;
};

可以發現這裡直接使用 xy 去獲取元素,然後和當前目標元素去做了對比。

這也就是為什麼 click 有時候可以點,有時候不可以的原因了,簡單說就是中心點被遮了就可以點,沒被遮就不可以點,還真是簡單粗暴 ?。這也導致了 click 的不穩定現象。

結果驗證

那我們來驗證下是不是如此,首先我們先建立一個非常小的遮擋元素,然後放在中央位置,測試下是不是會出問題。程式碼如下:

import style from './Covered-1.module.css';
const Covered = () => {
    return (
        <div className={style.parent} data-test-id='parent'>
            <div className={style.mask} data-test-id='mask'></div>
            <div className={style.child} data-test-id='child'></div>
        </div>
    );
};
export default Covered;
.parent {
    width: 100%;
    height: 100%;
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
}
.mask {
    background: rgb(0, 0, 0, 0.3);
    position: absolute;
    inset: 0;
}
.child {
    background: purple;
    width: 5px;
    height: 5px;
    z-index: 10;
}

測試用例就點選 mask 即可。

/// <reference types="cypress" />

context('Actions', () => {
    beforeEach(() => {
        cy.visit('http://localhost:3300/Covered-1');
    });

    it('Override', () => {
        cy.get('[data-test-id="mask"]').click();
    });
});

結果果然不出所料:

picture 1

為了嚴謹,我們再測試下另一個 case,我們將四周全部用元素遮擋住,只留下中心一點,然後點選,驗證下是不是可以正常。程式碼如下:

import style from './Covered-2.module.css';
const Covered = () => {
    return (
        <div className={style.parent} data-test-id='parent'>
            <div className={style.mask} data-test-id='mask'></div>
            <div className={style.child + ' ' + style.left} data-test-id='child'></div>
            <div className={style.child + ' ' + style.right} data-test-id='child'></div>
            <div className={style.child + ' ' + style.top} data-test-id='child'></div>
            <div className={style.child + ' ' + style.bottom} data-test-id='child'></div>
        </div>
    );
};
export default Covered;
.parent {
    width: 100%;
    height: 100%;
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
}
.mask {
    background: rgb(0, 0, 0, 0.3);
    position: absolute;
    inset: 0;
}
.child {
    background: purple;
    z-index: 10;
    position: absolute;
    top: 0;
    left: 0;
}
.left,
.right {
    width: 49%;
    height: 100%;
}
.right {
    right: 0;
    left: unset;
}
.top,
.bottom {
    height: 49%;
    width: 100%;
}
.bottom {
    top: unset;
    bottom: 0;
}

測試程式碼無需更改:

/// <reference types="cypress" />

context('Actions', () => {
    beforeEach(() => {
        cy.visit('http://localhost:3300/Covered-2');
    });

    it('Override', () => {
        cy.get('[data-test-id="mask"]').click();
    });
});

不出所料,果然可以點選。

picture 2

最後

說實在的 Cypress 這樣的遮擋檢查方式不太妥當,過於簡單粗暴而且很容易讓人困惑。理論上而言可以使用 layer 層層比對交叉區域來判定更為妥當。不知道是不是有什麼文件導致放棄了。

還有點選的方式感覺也可以再最佳化一下,比如提供了座標或者方位,那就應該以提供的座標或方位來做遮擋判定,現在遇到這種情況只能使用 force,然而使用了 force 這個測試的意義就少了一大半。