PAYATHON 2026

如何在 cucumberjs / puppeteer 中验证 selector 不存在?

支付小周

结论

selector 不存在时,不必使用 assert.throws()。具体写法取决于测试目的:

  • 检查元素当前是否不存在:使用 page.$(),并断言返回值为 null。
  • 等待元素从页面中消失:使用 page.waitForSelector(selector, { hidden: true })。
  • 验证等待元素出现会超时:使用异步 rejection assertion,例如 Node.js 的 assert.rejects(),不要使用 assert.throws()。

立即检查元素不存在

page.$() 会立即查询 DOM,找不到元素时返回 null:

const assert = require('node:assert/strict');

const selector =
    '[data-test-text-title-import-payment-file-widget-console="true"]';

const element = await page.$(selector);

assert.strictEqual(
    element,
    null,
    `Expected selector ${selector} not to exist`
);

在 Cucumber step 中,断言失败会直接抛出错误,当前步骤也会随之失败,无需手动返回 rejected Promise:

Then('the import payment widget is not present', async function () {
    const selector =
        '[data-test-text-title-import-payment-file-widget-console="true"]';

    const element = await this.page.$(selector);

    assert.strictEqual(element, null, `The widget is present: ${selector}`);
});

也可以直接判断查询结果:

const element = await page.$(selector);

if (element !== null) {
    throw new Error(`The widget is present: ${selector}`);
}

等待元素消失

如果页面会异步更新,立即调用 page.$() 可能查得太早。这种情况下,可以等待元素被移除或隐藏:

await page.waitForSelector(selector, {
    hidden: true,
    timeout: 30000
});

元素在超时时间内消失或隐藏时,Promise 会正常完成。否则,它会抛出超时错误,Cucumber 会将该步骤判定为失败。

hidden: true 接受两种状态:

  • 元素已经不在 DOM 中;
  • 元素仍在 DOM 中,但不可见。

如果要求元素必须从 DOM 中删除,可以等待查询结果变为 null:

await page.waitForFunction(
    selector => document.querySelector(selector) === null,
    { timeout: 30000 },
    selector
);

为什么原来的 assert.throws() 调用不正确

下面这段代码会先执行 page.waitForSelector():

assert.throws(await page.waitForSelector(selector));

因为使用了 await,传给 assert.throws() 的不是函数,而是等待结束后的结果。与此同时,page.waitForSelector() 返回的是 Promise。它超时时产生的是 Promise rejection,不是同步函数直接抛出的异常。

assert.throws() 通常用于同步代码:

assert.throws(() => {
    throw new Error('expected error');
});

异步 Promise 应使用 assert.rejects():

const assert = require('node:assert/strict');

await assert.rejects(
    () => page.waitForSelector(selector, { timeout: 1000 }),
    error => error?.name === 'TimeoutError'
);

这里无需添加 .then(callback)。断言成功时会正常完成,失败时则会抛出 AssertionError。

不过,这种写法验证的是“元素没有在指定时间内出现”,所以测试通常要等到超时,运行速度较慢。如果只是检查元素当前不存在,应优先使用 page.$();如果要验证元素最终消失,则使用 { hidden: true }。

关于更新代码中的 timeout

下面代码里的 timeout 不会让 page.$() 等待:

await page.$(selector, { timeout: settings._30000 });

page.$() 是即时查询方法,不使用这个超时选项。可以改成:

const element = await page.$(selector);

if (element === null) {
    return;
}

throw new Error('The widget is present');

如果需要等待,则使用:

await page.waitForSelector(selector, {
    hidden: true,
    timeout: settings._30000
});

原始 selector 的 title- import 之间还多了一个空格:

'[data-test-text-title- import-payment-file-widget-console="true"]'

这可能导致 selector 无效,或者查到错误的目标。需要确认正确形式是否为:

'[data-test-text-title-import-payment-file-widget-console="true"]'

备注:内容仅供参考。