Search code examples
javascriptnode.jspuppeteercodeceptjs

Does iterating table rows via Codeceptjs and Puppeteer require a Helper


I am trying to use the methods available within Codeceptjs to simply iterate rows of a table and select a row based on text existing in a particular cell of the current row being iterated.

The following code is in one of my page objects and partially works.

async selectSiteById(siteId) {
    I.waitForElement('table');
    for (let i = 1; i < 5; i++) {
      let val = await I.grabTextFrom(`tbody tr:nth-child(${i}) td:nth-child(2)`);
      if (val === siteId) {
        I.say('Val' + val + ' -- Site Id ' + siteId)
        within(`tbody tr:nth-child(${i + 1}) td:nth-child(1)`, () => {
          I.click('input');
        });
      }
      break;
    }
  },

The grabTextFrom pulls back the exact value I am after and stores it to val.

If the value of the parameter I pass in happens to be in the first row of the table, this works. However, my code only ever seems to be hitting the first row no matter what I do and I do not understand why?

Again, if the first row has the value of the parameter passed in, then my within method fires and checks the input box in the first column, exactly how I want it to do.

So, the two pieces of code I have for identifying the text I'm after in the given row works (mostly), and the code to click the check box in 'that' row also works.

If someone could help me understand why I cannot get this to loop through all rows of the table, I would hugely appreciate it.

Also, I can't seem to get codeceptjs to pull back the total number of tr in tbody as a simple array so I can use that as my length for the loop, so any pointers there would be awesome.

For this, I have tried - let rowCount = await I.grabNumberOfVisibleElements('tbody tr'); but does not seem to work.

I did try to move my break up one level as I initially thought it was in the wrong spot. When I did so, running my test results in the following error.

Sandbox to play around in -- Table check ALL Object: login I am on page "/login" I fill field "#username", "[email protected]" I fill field "#password", ***** I click "Sign in" I grab cookie I click "li[id="resources.admin.name"]" I click "Sites" I wait for element "table" I grab text from "tbody tr:nth-child(1) td:nth-child(2)" √ OK in 7254ms

tableFragment: selectSiteById
  I grab text from "tbody tr:nth-child(2) td:nth-child(2)"
  I grab text from "tbody tr:nth-child(3) td:nth-child(2)"   × "after all" hook: codeceptjs.afterSuite for "Check box of specific

table row" in 4672ms TypeError: Cannot read property '$$' of null (node:24032) UnhandledPromiseRejectionWarning: Cannot read property '$$' of null (node:24032) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag --unhandled-rejections=strict (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 7) (node:24032) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

-- FAILURES:

  1. Sandbox to play around in "after all" hook: codeceptjs.afterSuite for "Check box of specific table row": Cannot read property '$$' of null

Run with --verbose flag to see NodeJS stacktrace

Lastly, for basic interaction with a table, when using codeceptjs, are you basically required to use a helper and then write the code using native Puppeteer? (I'm using puppeteer in my project)

<<<<<<>>>>>> UPDATE

Can anyone help me understand why you can't seem to iterate rows of a table with CodeceptJS? I have to be missing something. Here is my current code from a PageObject class and I don't understand what the first iteration happens successfully, but then fails.

async test() {
    const totalRows = await I.grabAttributeFrom('tbody tr');
    I.say('Total Rows: ' + totalRows.length);
    for (let i = 1; i < totalRows.length; i++) {
      I.say('Current row is: ' + i);
      let str = await I.grabTextFrom(
        `tbody tr:nth-child(${i}) td:nth-child(2)`,
      );
      I.say('String value from table is: ' + str);
      if (str === 'IDR') {
        I.say('Match found in row: ' + i);
        within(`tbody tr:nth-child(1) td:nth-child(1) span span`, () => {
          I.click('input');
        });
        break;
      }
    }
    // I.say('Hello');
    //   let scores = [10, 15, 20, 30];
    //   for (let score of scores) {
    //     score += 3;
    //     I.say(score);
    //   }
  },

OUTPUT

Sandbox to play around in -- Table check ALL Object: login I am on page "/login" I fill field "#username", "[email protected]" I fill field "#password", ***** I click "Sign in" I grab cookie I click "li[id="resources.admin.name"]" I click "Sites" I grab attribute from "tbody tr" I wait 5 √ OK in 12226ms

Total Rows: 4 Current row is: 1 sitesPage: test I grab text from "tbody tr:nth-child(1) td:nth-child(2)" String value from table is: IDH Current row is: 2 I grab text from "tbody tr:nth-child(2) td:nth-child(2)" × "after all" hook: codeceptjs.afterSuite for "More goofing around" in 4682ms TypeError: Cannot read property '$$' of null (node:7180) UnhandledPromiseRejectionWarning: Cannot read property '$$' of null (node:7180) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag --unhandled-rejections=strict (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 7) (node:7180) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

-- FAILURES:

  1. Sandbox to play around in "after all" hook: codeceptjs.afterSuite for "More goofing around": Cannot read property '$$' of null

Run with --verbose flag to see NodeJS stacktrace

Thank you! Bob


Solution

  • I ended up going the helper route and built out some generic functions to work with tables. Thought I would share in the event this is helpful to anyone out there. Attempting to do this with the built in codeceptjs methods was unreliable.

    Puppeteer Helper class

    const { Helper } = codeceptjs;
    
    class Table extends Helper {
      // before/after hooks
      /**
       * @protected
       */
      _before() {
        // remove if not used
      }
    
      /**
       * @protected
       */
      _after() {
        // remove if not used
      }
    
      // add custom methods here
      // If you need to access other helpers
      // use: this.helpers['helperName']
    
      /**
       * Get the total rows displayed in a table contained within
       * the table body (tbody tr)
       */
      async getRowCount() { 
        //const browser = this.helpers['Puppeteer'].browser;
        const page = this.helpers['Puppeteer'].page;
    
        page.waitForSelector('tbody');
        const tableRows = 'tbody tr';
        let rowCount = await page.$$eval(tableRows, rows => rows.length);
        return rowCount;    
      }
    
      /**
       * When a table is present on the page, will check the box
       * in column 1 of the header row to select all items listed on 
       * the current table page (could be more than one page full)
       */
      async selectAll() {
        const page = this.helpers['Puppeteer'].page;
        page.waitForSelector('thead tr th:nth-child(1)');
        page.click('thead tr th:nth-child(1)');
      }
    
      /**
       * Checks the box in column 1 for the row containing the value
       * passed in (val), where that value exists in column (col)
       * @param {string} val The value you are looking for 
       * @param {number} col Which column the value will be in
       */
      async selectRow(val, col) {
        const page = this.helpers['Puppeteer'].page;
    
        page.waitForSelector('tbody');
        const tableRows = 'tbody tr';
        let rowCount = await page.$$eval(tableRows, rows => rows.length);
    
        for (let i = 0; i < rowCount; i++) {
          const str = await page.$eval(
            `${tableRows}:nth-child(${i + 1}) td:nth-child(${col})`,
            (e) => e.innerText
          )
          if (str === val) {
            await page.waitForSelector(`${tableRows}:nth-child(${i + 1}) td:nth-child(1)`);
            await page.click(`${tableRows}:nth-child(${i + 1}) td:nth-child(1)`);
            break;
          }
        }
      }
    
      /**
       * Will iterate through all rows displayed in the table and check the box
       * in column 1 for each row where the value in colum (col) matches.
       * @param {string} val The value passed in to look for
       * @param {number} col The column to find the value in
       */
      async selectAllRows(val, col) {
        const page = this.helpers['Puppeteer'].page;
    
        page.waitForSelector('tbody');
        const tableRows = 'tbody tr';
        let rowCount = await page.$$eval(tableRows, rows => rows.length);
    
        for (let i = 0; i < rowCount; i++) {
          const str = await page.$eval(
            `${tableRows}:nth-child(${i + 1}) td:nth-child(${col})`,
            (e) => e.innerText
          )
          if (str.includes(val)) {
            await page.waitForSelector(`${tableRows}:nth-child(${i + 1}) td:nth-child(1)`);
            await page.click(`${tableRows}:nth-child(${i + 1}) td:nth-child(1)`);
            continue;
          }
        }
      }
    
      /**
       * Locates the row containing the value passed in, in the
       * specified column (col)
       * @param {string} val Value passed in to look for in each row
       * @param {number} col The column to look for the value in
       */
      async editRow(val, col) {
        const page = this.helpers['Puppeteer'].page;
    
        page.waitForSelector('tbody');
        const tableRows = 'tbody tr';
        let rowCount = await page.$$eval(tableRows, rows => rows.length);
    
        for (let i = 0; i < rowCount; i++) {
          const str = await page.$eval(
            `${tableRows}:nth-child(${i + 1}) td:nth-child(${col})`,
            (e) => e.innerText
          )
          if (str === val) {
            await page.waitForSelector(`${tableRows}:nth-child(${i + 1}) td:nth-child(1)`);
            await page.click(`${tableRows}:nth-child(${i + 1}) td:nth-child(${col})`);
            break;
          }
        }
      }
    }
    
    module.exports = Table;