2

i have been struggling with some logic about using multiple page objects in one method, i couldnt find any idea to use like that logic, for example;

These are my methods in my page object called usersTable;

 get rolesAndStatusMenu() {
    return cy.get("#menu- > .MuiPaper-root > .MuiMenu-list>li");
  }

  get usersPartialRow() {
    return cy.get(".MuiTableBody-root>tr>td");
  }

settings(options: string) {
    return cy
      .get(
        "[style='position: fixed; z-index: 1300; inset: 0px;'] > .MuiPaper-root > .MuiList-root",
      )
      .contains(options);
  }

  menuButton(userId: string) {
    return cy.get(`.user_${userId}>td>button`);
  }

  userRow(userId?: string) {
    const userrow = ".MuiTableBody-root>tr";

    if (userId === undefined) {
      return cy.get(userrow);
    }

    return cy.get(userrow).get(`.user_${userId}`);
  }

im using userRow method in this test like that;

usersTable.userRow(userId).should("not.exist");

And for exaple im using my userMenu and settings method in this test;

usersTable.menuButton(userId).click();
usersTable.settings("Impersonate").click();

Let's come to the idea that I want to do but I can't find the way to do it;

usersTable.userRow(userId).settings.menuButton.click()

usersTable.userRow(userId).settings.impersonate.click()

Is there a any way to use like that ? All ideas are accepted

Update

I have one more page object, i define my usersTable component modal inside called usersPage page modal

    import { UsersTable } from "../components/UsersTable ";
    
    export class Users {
      visit() {
        return cy.visit("/users");
      }
      get headingText() {
        return cy.get(".MuiTypography-h5");
      }
      get inviteUserBtn() {
        return cy.get(".MuiGrid-root> .MuiButtonBase-root");
      }
      get inviteUserModal() {
        return cy.get(".MuiDialogContent-root");
      }
    get usersTable() {
    return new UsersTable();
  }
    }

So my code looks like this

usersPage.usersTable.menuButton(userId).click();
usersPage.usersTable.settings("Impersonate").click();
usersPage.visit();
usersPage.usersTable.menuButton(userId).click();
usersPage.usersTable.settings("Delete").click();
usersPage.usersTable.userRow(userId).should("not.exist");

For example using this way

usersPage.usersTable.userRow(userId).settings.menuButton.click()

So maybe i can create class inside UsersTable

export class UsersTable {
...
}
class userTableRow {
}
**and returning it in `UsersTable` or something like that ?**

Second Update

Now i create a class inside UsersTable file;

class UserRow {
  userRow(userId?: string) {
    const userrow = ".MuiTableBody-root>tr";
    if (userId === undefined) {
      return cy.get(userrow);
    }

    return cy.get(userrow).find(`.user_${userId}`);
  }
  get menuButton() {
    return this.userRow(`>td>button`); //Btw im not sure this one is working, i think something is wrong here;
  }
  get impersonate() {
    return cy
      .get(
        "[style='position: fixed; z-index: 1300; inset: 0px;'] > .MuiPaper-root > .MuiList-root",
      )
      .contains("Impersonate");
  }
  get delete() {
    return cy
      .get(
        "[style='position: fixed; z-index: 1300; inset: 0px;'] > .MuiPaper-root > .MuiList-root",
      )
      .contains("Delete");
  }
}

And for using this class returned in UsersTable class;

 userRow(userId?: string) {
    const userrow = ".MuiTableBody-root>tr";

    if (userId === undefined) {
      return cy.get(userrow);
    }

    return new UserRow(userId); **// but got error, it says Expected 0 arguments, but got 1.**
  }

If i use like this comment section;

 // get UserRow() {
  //   return new UserRow();  
  // }

I can able to reach everything inside user but i can't use my test like this;

   usersPage.usersTable.UserRow(userId).settings.menuButton.click()

or maybe

usersPage.usersTable.UserRow.userRow(userId).settings.menuButton.click()

But i can use like this;

 usersPage.usersTable.UserRow.menuButton.click()

How can i define userId?: string for UserRow userId is constantly changing every time, I get it from API inside test, So I can't define for sure.

  • Thanks for the replies, @Fody @SuchAnIgnorantThingToDo-UKR but can I do something like this? if i create a class called `UserTableRow` in the same file **(userTable.ts)** and return it, and define those functions here? If I need that line directly in the test; Maybe i can use `usersTable.userRow(userId).row` or `usersTable.userRow(userId).root` – Ali Unsal Albaz Jun 22 '22 at 07:38
  • Can you add some example code to the question to clarify? – Fody Jun 22 '22 at 07:39
  • Looks like the same problem will occur. `userRow(userId)` has no method or property `settings`. – Fody Jun 22 '22 at 08:13
  • So i can't create `userRow(user Id)` class and attend `settings` method in it ? isnt it possible? – Ali Unsal Albaz Jun 22 '22 at 08:33
  • From what I can see `userRow(user Id)` is a method on `UserTable` that currently returns `cy.get(userrow)`. Whatever it returns must have a method `settings` for `userRow(userId).settings()` to work. – Fody Jun 22 '22 at 08:45
  • Its fair, I'm out of ideas :/ – Ali Unsal Albaz Jun 22 '22 at 09:21
  • I'm not really sure why you like that pattern, it's not very flexible. There are loads of places in tests where you need to tweak/transform the results and `.then()` is very useful for that. – Fody Jun 22 '22 at 09:25

2 Answers2

0

The UserTable methods return the Cypress.Chainable type, so you will need to unwrap the result to pass it to the next method.

Also, it's returning the element but next method needs text content, so extract that as well.

usersTable.userRow(userId)
  .then((userIdElement: JQuery<HTMLElement>) => {  // unwrap Chainable
    const text = userIdElement.text()              // extract text
    usersTable.settings(text)
  })
  .then((settingsElement: JQuery<HTMLElement>) => {  // unwrap Chainable
    const text = settingsElement.text()              // extract text
    usersTable.menuButton(text).click()
  })

If any of the elements are HTMLInputElement, use userIdElement.val() instead.

An adjustment to userRow():

class UsersTable {

  ...

  userRow(userId?: string): Cypress.Chainable<JQuery<HTMLElement>> {  
    const userrow = ".MuiTableBody-root>tr";

    if (userId === undefined) {
      return cy.get(userrow);
    }

    return cy.get(userrow)
      .find(`.user_${userId}`)  // use find instead of get
  }
}

How to do it with Custom Commands instead of pageObject

Chaining is a natural code pattern for Cypress commands, so using Custom Commands

commands.js

/// <reference types="cypress" />

declare namespace Cypress {
  interface Chainable<Subject = any> {
    settings(options?: string): Chainable<JQuery<HTMLElement>>;
    menuButton(userId?: string): Chainable<JQuery<HTMLElement>>;
    userRow(userId?: string): Chainable<JQuery<HTMLElement>>;
  }
}

Cypress.Commands.add('settings', {prevSubject: 'optional'}, (subject: any, options?: string): Cypress.Chainable<JQuery<HTMLElement>>  => {
  if (options === undefined) {
    options = subject as string;
  }
  return cy.get("[style='position: fixed; z-index: 1300; inset: 0px;'] > .MuiPaper-root > .MuiList-root")
    .contains(options)
})

Cypress.Commands.add('menuButton', {prevSubject: 'optional'}, (subject: any, userId?: string): Cypress.Chainable<JQuery<HTMLElement>> => {
  if (userId === undefined) {
    userId = subject as string;
  }
  return cy.get(`.user_${userId}>td>button`);
})

Cypress.Commands.add('userRow', (userId?: string): Cypress.Chainable<JQuery<HTMLElement>> => {
  const userrow = ".MuiTableBody-root>tr";

  if (userId === undefined) {
    return cy.get(userrow);
  }

  return cy.get(userrow)
    .find(`.user_${userId}`)
})

test

it('tests with userId from userRow()', () => {
  const userId = '1'

  cy.userRow(userId)
    .settings()      // as child command, userId from previous command
    .menuButton()
    .click()
});

it('tests with userId hard-coded', () => {

  cy.settings('abc')    // as parent command, userId passed as parameter
    .menuButton()
    .click()
});
Fody
  • 23,754
  • 3
  • 20
  • 37
  • Thank you for answer, in this case im not going to edit my page objects right? But i need to edit my page object methods for using that logic i mentioned – Ali Unsal Albaz Jun 21 '22 at 23:57
  • I'm suggesting editing the test, pageObject is mostly the same (I added return type for clarity). – Fody Jun 21 '22 at 23:59
  • I tried a solution using modified pageObject, problem is to chain methods like `usersTable.userRow(userId).settings.menuButton.click()` the methods need to return `this` which is the instance of the pageObject. But then you can't return the result of the Cypress commands inside the method. – Fody Jun 22 '22 at 00:01
  • Yes, I was wondering if there is a way, but I couldn't find it despite my research, I guess it doesn't seem possible to chain it like this? – Ali Unsal Albaz Jun 22 '22 at 00:05
  • I thought about adding class instance properties for `userId` result and `setting` to capture results of each method, but above seemed cleaner solution. – Fody Jun 22 '22 at 00:07
  • Your solution is also a different point of view, of course, thank you for your suggestion. – Ali Unsal Albaz Jun 22 '22 at 00:10
0

To make class methods fluid, you will need to return this.

class UsersTable {

  settings(options: string) {
    cy.get(...).contains(options);
    return this                         // call next method on return value
  }

  menuButton(userId: string) {
    return cy.get(`.user_${userId}>td>button`); // cannot call another after this one
  }

  userRow(userId?: string) {
    cy.get(userrow).get(`.user_${userId}`);
    return this;                              // call next method on return value
  }
}

Should work,

usersTable.userRow(userId).settings().menuButton().click()

But now, where are the values from first two methods?

You would need to store them in the class

class UsersTable {

  userId: string = '';
  setting: string = '';

  settings(options: string) {
    cy.get(...).contains(options)
      .invoke('text')
      .then(text => this.setting = text)

    return this                        
  }

  menuButton() {                       // no parameter, take value from field
    return cy.get(`.user_${this.setting}>td>button`); 
  }

  userRow() {            // no parameter, take value from field
    cy.get(userrow).get(`.user_${this.userId}`)
      .invoke('text')
      .then(text => this.userId = text)

    return this;                              // call next method on return value
  }
}

But now have lost flexibility, methods are tightly coupled, not independent anymore.