I'm running into an issue with Cypress jQuery handling, specifically around accessing values without relying on *.then()* or *.each().*

What I’m trying to do is get a Cypress.Chainable<JQuery<HTMLElement>> reference to a table cell by matching a specific row’s text and a given column header.

Example reference jQuery + TypeScript

Here’s the function I’m working on

static findCellByRowTextColumnHeaderText(rowText: string, columnName: string) {
  const row = cy.get(`tr:contains(${rowText})`);
  const column = cy.get(`th:contains(${columnName})`);
  const columnIndex = ???;
  return row.find(`td:eq(${columnIndex})`);
}

I want this utility to help me keep the code DRY, for asserting content, clicking elements, etc.

But I keep running into problems trying to extract the actual column index value.

Example that doesn’t work as expected:

const columns = cy.get('th');
let columnIndex = -1;

columns.each((el, index) => {
  if (el.text().includes(columnName)) {
    columnIndex = index;
  }
  cy.log('columnIndex', columnIndex); // Logs correct value
});

cy.log('finalColumnIndex', columnIndex); // Always logs -1

I understand that Cypress commands are async, but is there a clean Cypress jQuery approach to extract this index without deeply nesting .then() chains?

Any suggestions or workarounds to resolve this would be appreciated.

Yeah, I’ve hit this same snag with Cypress when trying to get values like a column index outside of .each() or .then().

The key thing to remember is that Cypress commands are not just async, they’re queued. So columnIndex won’t update synchronously like in jQuery.

What worked for me was wrapping everything in a .then() chain once I had the elements. For example:

cy.get('th').then(($headers) => {
  const idx = [...$headers].findIndex((el) =>
    el.innerText.includes(columnName)
  );
  cy.get(`tr:contains(${rowText})`).find(`td:eq(${idx})`).should('exist');
});

It’s still Cypress + jQuery, but you keep the logic tight and avoid multiple .then().

So yes, you can solve this the Cypress jQuery way, just not with plain variables outside the chain.

This is one of those things I learned the hard way.

Cypress looks synchronous, but under the hood, it’s fully async, so trying to assign columnIndex outside .each() won’t work.

I ended up solving this using .within() instead of trying to extract values manually.

Something like:

cy.get('table').within(() => {
  cy.get('th').then(($ths) => {
    const index = [...$ths].findIndex((el) =>
      el.innerText.includes(columnName)
    );
    cy.contains('tr', rowText).find(`td:eq(${index})`).should('exist');
  });
});

Using .within() helped me keep the logic scoped and readable, and I didn’t have to worry about Cypress trying to resolve commands too early.

Might be a cleaner Cypress jQuery-style workaround than trying to manage global variables.

I’ve built a lot of reusable Cypress utils for our test suite, and my take is: if you want true DRY + clarity, don’t fight Cypress’s async nature , embrace it with a custom command.

Here’s how I tackled the Cypress jQuery cell lookup:

Cypress.Commands.add('getCellByHeaderAndRowText', (rowText, columnName) => {
  cy.get('table').then(($table) => {
    const $headers = $table.find('th');
    const colIndex = [...$headers].findIndex((el) =>
      el.innerText.includes(columnName)
    );
    const $row = [...$table.find('tr')].find((tr) =>
      tr.innerText.includes(rowText)
    );
    const $cell = $row?.querySelectorAll('td')[colIndex];
    cy.wrap($cell);
  });
});

Then in your test:

cy.getCellByHeaderAndRowText('Some Value', 'Status').should('contain', 'Active');

This avoids nesting and keeps your logic isolated. It’s worked beautifully in our React app where class names and structure shift often.