Combining Cypress, Typescript and Page Object concepts into a simple easy to read end-2-end test suite.

export class PageObject {
    constructor(anything: any) {
    }
    wrap = (anything: any) => this
}
export class LoginPageObject extends PageObject{
    clickSignIn =  () => this.wrap(cy.get('[data-cy="signin"]').click())
    enterEmail = (email: string) => this.wrap(cy.get('[data-cy="email"]').type(email))
    enterPassword = (password: string) => this.wrap(cy.get('[data-cy="password"]').type(password))
    shouldSeeAuthenticationFailedAlert = () => this.wrap(cy.get('[data-cy="authentication-failed"]').should('be.visible'))
    shouldSeeDashboard = () => this.wrap(cy.get('[data-cy="dashboard"]').should('be.visible'))
}

describe('Login', function () {
    it('should fail login with message', () => {
        goToLogin()
            .enterEmail("user@example.com")
            .enterPassword("not-right")
            .clickSignIn()
            .shouldSeeAuthenticationFailedAlert()
    });

    it('should login', () => {
        goToLogin()
            .enterEmail("user@example.com")
            .enterPassword("real-passw0rd")
            .clickSignIn()
            .shouldSeeDashboard()
    });
});

Cypress Style Chainable Objects

Using this->wrap in LoginPageObject creates a chainable object (Fluent Interface) keeping the tests concise and specific

Extending the cypress chainable object would require deep cloning of the cypress objects leading to unwanted complexity. So instead we have opted to encapsulate all of the cypress calls in the PageObjects including the contriversal decision to include the assertions there.

Typescript

Instead of returning this using this->wrap we can return another page object. This makes sense for thing like navigation or button events that will change the context.

export class NavigationPageObject extends PageObject {
    clickNewDocument  = () => new DocumentPageObject(cy.get('[data-cy="newDocument"]').click())
    clickDashboard  = () => new DashboardPageObject(cy.get('[data-cy="dashboard"]').click())
}

export class DocumentPageObject extends PageObject {
    clickCreate = () => this.wrap(cy.get('[data-cy="create"]').click())
    enterTitle = (title: string) => this.wrap(cy.get('[data-cy="title"]').type(title))
}
    it('should Save', () => {
        navigationPageObject
            .clickNewDocument()
            .enterTitle("Test Document")
            .clickCreate()
    });

Page Object

The goal of the Page Object pattern is to handle all the programming interactions with the web page and make the simple to read and write test against.

In this simple example, all the things a person can do or interact with on the login page are included in the Page Object. Exposing a self documenting API.

Hints

Use live templates of snippets for common page object methods

I use :

cyclick:
$METHOD$ = () => this.wrap(cy.get('[data-cy="$END$"]').click())

cytype:
$METHOD$ = ($VAR$: string) => this.wrap(cy.get('[data-cy="$END$"]').type($VAR$))

Page Objects don’t need to be a whole page, they can represent any logical components in your application