node.jsweb-scrapingpuppeteer

Puppeteer - Cannot Target Checkbox on Hotel Website


Problem: Using Puppeteer, I cannot click Available Hotels check-box in hotel website (https://www.wyndhamhotels.com/hotels/chicago-il-usa?brand_id=HR&checkInDate=10/31/2023&checkOutDate=11/5/2023&useWRPoints=false&children=0&adults=3&rooms=2&loc=ChIJ7cv00DwsDogRAMDACa2m4K8&sessionId=1694533511).

wyndham ss

I tried the following below but they all didn't work for clicking "Available Hotels" check-box.

Entire code:

const puppeteer = require('puppeteer-extra');
const constants = require('./wyndham_constants.js');
const fs = require('fs/promises');

// * Main Function
let browser;
async function wyndhamScan() {
    browser = await puppeteer.launch({
    headless: false
    });
    const [page] = await browser.pages();
    const ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36";
    await page.setUserAgent(ua);
    await page.setViewport({ width: 1065, height: 10700 });

    // Go to the URL website
    await page.goto('https://www.wyndhamhotels.com/wyndham', { waitUntil: "domcontentloaded" });

    // Enter destination
    await page.type('[name="destination"]', 'Chicago, IL, USA' );
    await page.waitForTimeout(1000);

    // Click Search button
    await page.click('.search-btn.btn-primary');
    await page.waitForNavigation();
    await page.waitForTimeout(5000);


    // ------------------------------------------ Re-directed to Results page ----------------------------------------
    
    // Click "Available Hotels"
    // ! WIP - below does not work
    // await page.click('[class^="cmp-availability-filter"] .cmp-item-filter__span');
    // await page.click('.cmp--filter.checkbox .cmp-item-filter__span');
    // await page.click('#availabilityfilter #chk-show-avail');
    // await page.click('#availabilityfilter input[class="checkbox"]');
    // await page.click('#availabilityfilter span[class="cmp-item-filter__span"]');
    // await page.click('[class^="cmp-availability-filter"] input[class="checkbox"]');
    // await page.click('.cmp-item-filter__span');

    // Nothing clicked for this and program returned TimeoutError:
    // const checkboxs = document.querySelectorAll('[class^="cmp-availability-filter"] .cmp-item-filter .checkbox');
    // checkboxs[0].click()
    
    // This returned Node is either not clickable or not an HTMLElement:
    // await page.click('[class^="cmp-availability-filter"] .cmp-item-filter .checkbox');
    
      const el = await page.waitForSelector("#chk-show-avail");

      try {
        await page.waitForFunction(() => {
            return !document.querySelector("#chk-show-avail").disabled;
        });
        // Clicking an element by selector
        await page.click('#chk-show-avail');
        // Wait for a specific timeout (60 seconds in this case)
        await page.waitForTimeout(60000);
    } catch (error) {
        console.error(error);
    } finally {
        await browser?.close();
    }

}

// * Call the main function
(async () => {
    console.log('Starting the Wyndham scraper program...');
    await wyndhamScan()
        .catch(err => console.error(err))
        .finally(() => browser?.close());
})();

Solution

  • This page has complex behavior. The search results don't populate consistently, probably due to rate limiting, so I can't 100% verify this answer, but hopefully it'll point you in the right direction.

    page.click() is a complex multi-step operation that moves the mouse to the coordinates of the element and issues a trusted click. There are at least two confounding factors that make page.click() potentially the wrong tool here:

    1. The checkbox starts out disabled until the search results populate, so clicking it while it's disabled won't do anything. If the hotel list never loads, the checkbox is never disabled.
    2. There is a loading overlay that covers the whole screen, so clicking the element while behind this overlay will fail.

    Problem 1 is a matter of waiting for the disabled attribute to be false on the element using waitForFunction.

    If you don't need a trusted click, verify that document.querySelector("#chk-show-avail").click() successfully toggles the selector in the browser console, without Puppeteer. In this case, it does, so I'd use that in your Puppeteer script as well. Using the native browser click bypasses visibility, solving problem 2.

    Additionally, you have a race condition when using a click that triggers a navigation. The navigation promise must be set before the click is fired, otherwise it may time out.

    Putting these three steps together:

    const puppeteer = require("puppeteer"); // ^21.0.2
    const {setTimeout} = require("node:timers/promises");
    
    const url = "https://www.wyndhamhotels.com/wyndham";
    
    const navClick = async (page, sel) => {
      const nav = page.waitForNavigation();
      await page.click(sel);
      return nav;
    };
    
    let browser;
    (async () => {
      browser = await puppeteer.launch({headless: false});
      const [page] = await browser.pages();
      await page.goto(url);
      await page.type('[name="destination"]', "Chicago, IL, USA");
      await navClick(page, ".search-btn.btn-primary");
      const el = await page.waitForSelector("#chk-show-avail");
      await page.waitForFunction(`
        !document.querySelector("#chk-show-avail").disabled
      `);
      await el.evaluate(el => el.click());
    
      // not part of the logic; temporarily added so you can see
      // the box was ticked (screenshots can be hidden by modals)
      await setTimeout(60_000);
    })()
      .catch(err => console.error(err))
      .finally(() => browser?.close());
    

    Disclosure: I'm the author of the blog post linked above.