Search code examples
typescriptcypresspageobjects

Cypress page object model pattern. Extending the elements property


I have the following page object model representing a widget in my app

/**
 * Contains common actions for all widgets
 */

export default abstract class AbstractWidget {
  private widgetId: number;

  elements = {
    widget: () => cy.getDataTestId(`widgetbox-container-${this.widgetId}`),
  };

  constructor(widgetId: number) {
    this.widgetId = widgetId;
  }
  
}

And I have a concrete class extending this class where I would like to append extra elements specific to this widget to the elements property

import AbstractWidget from './AbstractWidget';

export default class SpecificWidget extends AbstractWidget {
    elements = {
        ...super.elements,
        assetSearch: () => cy.getDataTestId('assetSearch'),
    };


    constructor(widgetId: number) {
        super(widgetId);
    }
}

however, when I try to spread the elements from the abstract super class

    elements = {
        ...super.elements,
        assetSearch: () => cy.getDataTestId('assetSearch'),
    };

it results in the typescript error

TS2340: Only public and protected methods of the base class are accessible via the  super  keyword.

What am I doing wrong?


Solution

  • This seems to be a result of this issue
    Accessing property on super should be a type error #35314 Nov 24, 2019

    If you follow the TS Playground link, it opens with the latest TS version and console.log(super.n) shows an error. If you change the version to 3.7.5 the error goes away.

    But if you look at the Handbook Overriding Methods, the equivalent access for methods is permitted, so the above change seems to have created an anomaly

    class Base {
      greet() {
        console.log("Hello, world!");
      }
    }
     
    class Derived extends Base {
      greet(name?: string) {
        if (name === undefined) {
          super.greet();
        } else {
          console.log(`Hello, ${name.toUpperCase()}`);
        }
      }
    }
    

    Specifically in Cypress you can either use this.elements instead of super.elements, since the derived class inherits the base class element property:

    Testing (with string return values instead of Chainers)

    export default class SpecificWidget extends AbstractWidget {
      elements = {
        ...this.elements,
        assetSearch: () => 'assetSearch',
      }
    
      constructor(widgetId: number) {
        super(widgetId)
      }
    }
    
    export default abstract class AbstractWidget {
      private widgetId: number
    
      elements = {
        widget: () => `widgetbox-container-${this.widgetId}`,
      }
    
      constructor(widgetId: number) {
        this.widgetId = widgetId
      }
    }
    
    const specific = new SpecificWidget(1)
    expect(specific.elements.widget()).to.eq('widgetbox-container-1')
    expect(specific.elements.assetSearch()).to.eq('assetSearch')
    

    enter image description here


    Or you can create a "helper method" in the base class

    export default abstract class AbstractWidget {
      private widgetId: number
    
      elements = {
        widget: () => `widgetbox-container-${this.widgetId}`,
      }
    
      getElements() {
        return this.elements
      }
    
      constructor(widgetId: number) {
        this.widgetId = widgetId
      }
    }
    
    export default class SpecificWidget extends AbstractWidget {
      elements = {
        ...super.getElements(),
        assetSearch: () => 'assetSearch',
      }
    
      constructor(widgetId: number) {
        super(widgetId)
      }
    }
    

    An additional note, returning Chainers like this

    widget: () => cy.getDataTestId(`widgetbox-container-${this.widgetId}`),
    

    can result in unexpected problems if the returned Chainer gets invalidated due to page changes.

    See Variables and aliases - Return values

    Return Values
    You cannot assign or work with the return values of any Cypress command. Commands are enqueued and run asynchronously.

    It's ok if you intend to chain immediately with no page actions between calling widgit() and using the result, but that means anyone using the class has to remember that caveat.

    Cypress made a change to aliases to defaullt to type: query specifically for this problem.

    If an alias becomes stale when used, the chainer is re-run to get a fresh copy of the query result. But this won't happen automatically with your page-object methods.