Puppeteer:page.click 和 page.evaluate click 回退

Puppeteer: page.click and page.evaluate click fallbacks

提问人:Woden 提问时间:11/15/2023 最后编辑:Woden 更新时间:11/15/2023 访问量:22

问:

如标题所述,大多数情况下 page.click() 有效。但是,有时它会单击并且页面不会对操作做出反应。有两个详细的方案:

  1. page.click() 不起作用并引发错误。在这种情况下,我可以回退以正确评估点击。
  2. page.click() 确实有效,并且不会抛出错误。页面对点击没有反应。在这种情况下,我不知道如何回退到另一种点击方法。

评估点击也有可能不起作用。因此,我认为最好的解决方案是在无法点击时回退到对方。这是我的代码:

async clickElement(
    page: Page,
    projectName: string,
    title: string,
    selector: string,
    timeout = 1000,
    preventNavigation = false
  ): Promise<boolean> {
    Logger.log(selector, 'CSSClickStrategy.clickElement');

    // Ensure the selector is present before proceeding
    // it could be a navigation before awaiting the selector
    await Promise.race([
      page.waitForSelector(selector, { timeout, visible: true }),
      page.waitForNavigation({ timeout }),
    ]);
    //TODO: how to determine to switch between the two click methods
    // Add navigation prevention if required
    if (preventNavigation) {
      this.preventNavigationOnElement(page, selector);
      return this.evaluateClick(page, projectName, title, selector);
    } else {
      // const result = await this.normalClick(page, projectName, title, selector);
      const result = await this.evaluateClick(
        page,
        projectName,
        title,
        selector
      );
      if (!result) {
        // return await this.evaluateClick(page, projectName, title, selector);
        return await this.normalClick(page, projectName, title, selector);
      } else {
        return result;
      }
    }
  }

  private async preventNavigationOnElement(page: Page, selector: string) {
    await page.evaluate((sel) => {
      const element = document.querySelector(sel);
      if (element) {
        element.addEventListener('click', (e) => e.preventDefault());
      }
    }, selector);
  }

  private async evaluateClick(
    page: Page,
    projectName: string,
    title: string,
    selector: string,
    timeout = 10000
  ): Promise<boolean> {
    try {
      await Promise.race([
        page.evaluate((sel) => {
          const element = document.querySelector(sel) as HTMLElement;
          element?.click();
        }, selector),
        new Promise((_, reject) =>
          setTimeout(() => reject(new Error('Timeout exceeded')), timeout)
        ),
      ]);
      Logger.log(
        `Clicked using page.evaluate for selector: ${selector}`,
        'CSSClickStrategy.clickElement'
      );
      return true;
    } catch (error) {
      Logger.error(error.message, 'CSSClickStrategy.evaluateClick');
      return false;
    }
  }

  private async normalClick(
    page: Page,
    projectName: string,
    title: string,
    selector: string,
    timeout = 10000
  ): Promise<boolean> {
    try {
      await Promise.race([
        page.click(selector, { delay: 100 }),
        new Promise((_, reject) =>
          setTimeout(() => reject(new Error('Timeout exceeded')), timeout)
        ),
      ]);
      Logger.log(
        `Clicked using page.click for selector: ${selector}`,
        'CSSClickStrategy.clickElement'
      );
      return true;
    } catch (error) {
      Logger.error(error.message, 'CSSClickStrategy.clickElement');
      return false;
    }
  }

非常感谢任何帮助。

打字稿 傀儡师

评论

0赞 ggorlen 11/15/2023
你犯了什么错误?你能分享一个实际的网站,以便这个例子是可重现的吗?很多时候,没有必要实际点击一个元素来实现一个潜在的结果,所以最终目标是什么的背景是好的。谢谢。.click()
0赞 Woden 11/16/2023
@ggorlen 好问题。这可能是我设置的超时或任何其他意外错误,所以我无法真正提供示例。但是,最重要的部分是,如果不抛出错误,两次点击都可能无法按预期工作。我写了一个关于更多上下文和我正在尝试做的事情的答案。如果您还有疑问,请告诉我。谢谢。

答:

0赞 Woden 11/15/2023 #1

我已经想出了一种根据某些条件相互回退的方法。例如,我发现大多数时候评估点击都可以工作,但在第三方电子商务支付网关上不起作用。因此,我在支付页面上使用 page.click()。否则,默认情况下使用评估点击。另一个发现是,在隐身上下文下有一个选项卡,评估点击无法正常工作。这是我的解决方案代码,基于条件和遇到错误时相互回退:

async clickElement(
    page: Page,
    projectName: string,
    title: string,
    selector: string,
    timeout = 1000,
    preventNavigation = false
  ): Promise<boolean> {
    Logger.log(selector, 'CSSClickStrategy.clickElement');
    const domain = new URL(
      this.sharedService.getProjectDomain(projectName, {
        absolutePath: undefined,
        name: title,
      })
    ).hostname;

    // Ensure the selector is present and visible before proceeding
    await Promise.race([
      page.waitForSelector(selector, { timeout, visible: true }),
      page.waitForNavigation({ timeout }),
    ]);

    // Determine the click method based on conditions
    // only one page means checking datalayer; two pages mean checking with gtm preview mode
    // if the current page is not the same as the domain, then it's a third-party gateway
    const useNormalClick =
      (await page.browserContext().pages()).length === 1 ||
      !page.url().includes(domain);

    if (preventNavigation) {
      this.preventNavigationOnElement(page, selector);
    }

    if (useNormalClick) {
      return await this.attemptClick(
        page,
        projectName,
        title,
        selector,
        this.normalClick
      );
    } else {
      return await this.attemptClick(
        page,
        projectName,
        title,
        selector,
        this.evaluateClick
      );
    }
  }

  async attemptClick(
    page: Page,
    projectName: string,
    title: string,
    selector: string,
    clickMethod: (
      page: Page,
      projectName: string,
      title: string,
      selector: string
    ) => Promise<boolean>
  ) {
    const result = await clickMethod.call(
      this,
      page,
      projectName,
      title,
      selector
    );
    if (!result) {
      // Fallback to the other click method
      return await (clickMethod === this.normalClick
        ? this.evaluateClick
        : this.normalClick
      ).call(this, page, projectName, title, selector);
    }
    return result;
  }
 // other private methods are in the question content

以下是我的启动设置以供参考:

 const browser = await puppeteer.launch({
      headless: headless === 'new' ? 'new' : false,
      defaultViewport: null,
      ignoreHTTPSErrors: true,
      args: [
        '--window-size=1440,900',
        '--no-sandbox',
        '--disable-setuid-sandbox',
        '--disable-dev-shm-usage',
        '--disable-accelerated-2d-canvas',
        '--disable-gpu',
      ],
    });