Deje de usar Page Objects (PO) y comience a usar App Actions

Hola, Khabrovites. Para los futuros estudiantes del curso JavaScript QA Engineer, preparamos una traducción de material útil.



También invitamos a todos a participar en el
seminario web abierto sobre el tema "Lo que un evaluador debe saber sobre JS". La lección considerará las características de JS que debe tener en cuenta todo el tiempo al escribir pruebas.






— . -, page objects, . , page objects — , . test runner Cypress.io, .





Page objects

Page Objects 1, 2 , . ad-hoc , , . , Selenium Wiki.





public class LoginPage {
  private final WebDriver driver;

  public LoginPage(WebDriver driver) {
    this.driver = driver;

    // Check that we're on the right page.
    if (!"Login".equals(driver.getTitle())) {
      // Alternatively, we could navigate to the
      // login page, perhaps logging out first
      throw new IllegalStateException("This is not the login page");
    }
  }

  // The login page contains several HTML elements
  // that will be represented as WebElements.
  // The locators for these elements should only be defined once.
  By usernameLocator = By.id("username");
  By passwordLocator = By.id("password");
  By loginButtonLocator = By.id("login");

  // The login page allows the user to type their
  // username into the username field
  public LoginPage typeUsername(String username) {
    // This is the only place that "knows" how to enter a username
    driver.findElement(usernameLocator).sendKeys(username);

    // Return the current page object as this action doesn't
    // navigate to a page represented by another PageObject
    return this;
  }
  // other methods
  //  - typePassword
  //  - submitLogin
  //  - submitLoginExpectingFailure
  //  - loginAs
}
      
      



Page Objects :









Page Objects:





public void testLogin() {
  LoginPage login = new LoginPage(driver);
  login.typeUsername('username')
  login.typePassword('username')
  login.submitLogin()
}
      
      



PageObjects API HTML. PageObjects HTML.





Tests
-----------------
  Page Objects
~ ~ ~ ~ ~ ~ ~ ~ ~
    HTML UI
-----------------
Application code
      
      



  . 





1. HTML . 





2. HTML page objects . 





3. PO





HTML UI , HTML- DOM — render    - DOM one to one.  HTML.





Page objects HTML , , ~ ~. , , - . , DOM , .





, ,   HTML , .





, page objects — , .





Page objects Cypress

Page Objects Cypress. “Deep diving PageObject pattern and using it with Cypress”. PageObject SignInPage LoginPage Selenium, .





class SignInPage {
  visit() {
    cy.visit('/signin');
  }

  getEmailError() {
    return cy.get(`[data-testid=SignInEmailError]`);
  }

  getPasswordError() {
    return cy.get(`[data-testid=SignInPasswordError]`);
  }

  fillEmail(value) {
    const field = cy.get(`[data-testid=SignInEmailField]`);
    field.clear();
    field.type(value);

    return this;
  }

  fillPassword(value) {
    const field = cy.get(`[data-testid=SignInPasswordField]`);
    field.clear();
    field.type(value);

    return this;
  }

  submit() {
    const button = cy.get(`[data-testid=SignInSubmitButton]`);
    button.click();
  }
}

export default SignInPage;
      
      



“Home page”,  SignInPage



Page Object.





import Header from './Headers';
import SignInPage from './SignIn';

class HomePage {
  constructor() {
    this.header = new Header();
  }

  visit() {
    cy.visit('/');
  }

  getUserAvatar() {
    return cy.get(`[data-testid=UserAvatar]`);
  }

  goToSignIn() {
    const link = this.header.getSignInLink();
    link.click();

    const signIn = new SignInPage();
    return signIn;
  }
}

export default HomePage;
      
      



PageObject



, , , object-oriented . :





import HomePage from '../elements/pages/HomePage';

describe('Sign In', () => {
  it('should show an error message on empty input', () => {
    const home = new HomePage();
    home.visit();

    const signIn = home.goToSignIn();

    signIn.submit();

    signIn.getEmailError()
      .should('exist')
      .contains('Email is required');

    signIn
      .getPasswordError()
      .should('exist')
      .contains('Password is required');
  });

  // more tests
});
      
      



Cypress JavaScript, , .





object-oriented PageObject. Cypress Custom Commands, . , “login”.





// in cypress/support/commands.js
Cypress.Commands.add('login', (username, password) => {
  cy.get('#login-username').type(username)
  cy.get('#login-password').type(password)
  cy.get('#login').submit()
})
      
      



, , built-in



.





// cypress/integration/spec.js
it('logs in', () => {
  cy.visit('/login')
  cy.login('username', 'password')
})
      
      



, , JavaScript ( , check step .





// cypress/integration/util.js
export const login = (username, password) => {
  cy.get('#login-username').type(username)
  cy.get('#login-password').type(password)
  cy.get('#login').submit()
}
      
      



// cypress/integration/spec.js
import { login } from './util'

it('logs in', () => {
  cy.visit('/login')
  login('username', 'password')
})
      
      



Page Objects

, PageObject



, .





  • PageObject



    , . PageObjects



    , .





  • PageObject



    , . ..





  • PageObject



    , — , , .





  • PageObject



    , .





! Page Objects, "App Actions", . , App Actions , .





TodoMVC. , todos. Cypress — , .





describe('TodoMVC', function () {
  // set up these constants to match what TodoMVC does
  let TODO_ITEM_ONE = 'buy some cheese'
  let TODO_ITEM_TWO = 'feed the cat'
  let TODO_ITEM_THREE = 'book a doctors appointment'

  beforeEach(function () {
    cy.visit('/')
  })

  context('New Todo', function () {
    it('should allow me to add todo items', function () {
      cy.get('.new-todo').type(TODO_ITEM_ONE).type('{enter}')
      cy.get('.todo-list li').eq(0).find('label').should('contain', TODO_ITEM_ONE)
      cy.get('.new-todo').type(TODO_ITEM_TWO).type('{enter}')
      cy.get('.todo-list li').eq(1).find('label').should('contain', TODO_ITEM_TWO)
    })

    // more tests for adding items
    // - adds items
    // - should clear text input field when an item is added
    // - should append new items to the bottom of the list
    // - should trim text input
    // - should show #main and #footer when items added
  })
})
      
      



“New Todo” , <input class="new-todo" />



 shortcuts. , .





“buy some cheese” ( ), , .





 





" " (“Mark all as completed”). , .





<input
  className='toggle-all'
  type='checkbox'
  onChange={this.toggleAll}
  checked={activeTodoCount === 0} />
      
      



— todo .toggle-all



? cy.createDefaultTodos().as('todos')



, UI , , .





// cypress/support/commands.js
const TODO_ITEM_ONE = 'buy some cheese'
const TODO_ITEM_TWO = 'feed the cat'
const TODO_ITEM_THREE = 'book a doctors appointment'

Cypress.Commands.add('createDefaultTodos', function () {
  cy.get('.new-todo')
    .type(`${TODO_ITEM_ONE}{enter}`)
    .type(`${TODO_ITEM_TWO}{enter}`)
    .type(`${TODO_ITEM_THREE}{enter}`)
    .get('.todo-list li')
})
      
      



createDefaultTodos



.





// cypress/integration/spec.js
context('Mark all as completed', function () {
  beforeEach(function () {
    cy.createDefaultTodos().as('todos')
  })

  it('should allow me to mark all items as completed', function () {
    // complete all todos
    // we use 'check' instead of 'click'
    // because that indicates our intention much clearer
    cy.get('.toggle-all').check()

    // get each todo li and ensure its class is 'completed'
    cy.get('@todos').eq(0).should('have.class', 'completed')
    cy.get('@todos').eq(1).should('have.class', 'completed')
    cy.get('@todos').eq(2).should('have.class', 'completed')
  })

  // more tests
  // - should allow me to clear the complete state of all items
  // - complete all checkbox should update state when items are completed / cleared
})
      
      







:





  1. UI — , "New Todo".





  2. , .





— - . " "  (“Mark all as completed”)  4 5 .





App Actions





Application Actions. , , Page Object App Actions repo bahmutov/test-todomvc-using-app-actions.





, , , . Cypress , . , , , , , window.





// app.jsx code
var model = new app.TodoModel('react-todos');

if (window.Cypress) {
  window.model = model
}
      
      



model window model.addTodo



, js/todoModel.js



.





// js/todoModel.js
// Model: keeps all todos and has methods to act on them
app.TodoModel = function (key) {
  this.key = key
  this.todos = Utils.store(key)
  this.onChanges = []
}
app.TodoModel.prototype.addTodo = function (title) {
  this.todos = this.todos.concat({
    id: Utils.uuid(),
    title: title,
    completed: false
  });

  this.inform();
};
app.TodoModel.prototype.inform = ...
app.TodoModel.prototype.toggleAll = ...
// other methods
      
      



, Page Object,  todos



cy.createDefaultTodos().as('todos')



model.addTodo



, , “api” . cy.window() , ,  model



.invoke() , addTodo



.





beforeEach(function () {
  cy.window().its('model').invoke('addTodo', TODO_ITEM_ONE)
  cy.window().its('model').invoke('addTodo', TODO_ITEM_TWO)
  cy.window().its('model').invoke('addTodo', TODO_ITEM_THREE)
  cy.get('.todo-list li').as('todos')
})
      
      



— 1 , 3 , . , — Cypress , . TodoModel.prototype.addTodo



, .





// js/todoModel.js
app.TodoModel.prototype.addTodo = function (...titles) {
  titles.forEach(title => {
    this.todos = this.todos.concat({
      id: Utils.uuid(),
      title: title,
      completed: false
    });
  })

  this.inform();
};
      
      



// cypress/integration/spec.js
beforeEach(function () {
  cy.window().its('model').invoke('addTodo',
    TODO_ITEM_ONE, TODO_ITEM_TWO, TODO_ITEM_THREE)
  cy.get('.todo-list li').as('todos')
})
      
      



, ? , . , ! , , .





App Actions DevTools , "Your app", . .





app actions , . , cy



.





const addDefaultTodos = () => {
  cy.window().its('model').invoke('addTodo',
    TODO_ITEM_ONE, TODO_ITEM_TWO, TODO_ITEM_THREE)
  cy.get('.todo-list li').as('todos')
}

beforeEach(addDefaultTodos)
      
      



Cypress , addDefaultTodos



  require import , spec-. addDefaultTodos



, JSDoc, .





// utils.js
const TODO_ITEM_ONE = 'buy some cheese'
const TODO_ITEM_TWO = 'feed the cat'
const TODO_ITEM_THREE = 'book a doctors appointment'

/**
 * Creates default todo items using application action.
 * @example
 *  import { addDefaultTodos } from './utils'
 *  beforeEach(addDefaultTodos)
 */
export const addDefaultTodos = () => {
  cy.window().its('model').invoke('addTodo',
    TODO_ITEM_ONE, TODO_ITEM_TWO, TODO_ITEM_THREE)
  cy.get('.todo-list li').as('todos')
}
      
      



app actions — JavaScript-, — .





 

TodoMVC, . , , . . Cypress .





context('Persistence', function () {
  it('should persist its data', function () {
    // mimicking TodoMVC tests
    // by writing out this function
    function testState () {
      cy.get('@firstTodo').should('contain', TODO_ITEM_ONE)
        .and('have.class', 'completed')
      cy.get('@secondTodo').should('contain', TODO_ITEM_TWO)
        .and('not.have.class', 'completed')
    }

    cy.createTodo(TODO_ITEM_ONE).as('firstTodo')
    cy.createTodo(TODO_ITEM_TWO).as('secondTodo')
    cy.get('@firstTodo').find('.toggle').check()
    .then(testState)

    .reload()
    .then(testState)
  })
})
      
      



testState



— , — . .





, , ? , ! , . Item — should allow me to mark items as complete



, :





context('Item', function () {
  it('should allow me to mark items as complete', function () {
    cy.createTodo(TODO_ITEM_ONE).as('firstTodo')
    cy.createTodo(TODO_ITEM_TWO).as('secondTodo')

    cy.get('@firstTodo').find('.toggle').check()
    cy.get('@firstTodo').should('have.class', 'completed')

    cy.get('@secondTodo').should('not.have.class', 'completed')
    cy.get('@secondTodo').find('.toggle').check()

    cy.get('@firstTodo').should('have.class', 'completed')
    cy.get('@secondTodo').should('have.class', 'completed')
  })
})
      
      



. , , , cypress-testing-library - , .





.





cy.createTodo(TODO_ITEM_ONE).as('firstTodo')
cy.createTodo(TODO_ITEM_TWO).as('secondTodo')
cy.get('@firstTodo').find('.toggle').check()
      
      



, , app actions. addTodo



, class="toggle"



, , «».





// spec.js
import { addTodos } from './utils';

addTodos(TODO_ITEM_ONE, TODO_ITEM_TWO)
cy.get('.todo-list li').eq(0).find('.toggle').check()
      
      



todoModel.js



, todo



.





app.TodoModel.prototype.toggle = function (todoToToggle) {
  this.todos = this.todos.map(function (todo) {
    return todo !== todoToToggle ?
      todo :
      Utils.extend({}, todo, {completed: !todo.completed});
  });

  this.inform();
};
      
      



model.toggle



, completed



? Cypress , DevTools. , , DevTools test runner



, “Your App” . , , model.toggle(model.todos[0])



«».





app actions toggle. , , , .





/**
 * Toggle given todo item. Returns chain so you can attach more Cypress commands
 * @param {number} k index of the todo item to toggle, 0 - first item
 * @example
 import { addTodos, toggle } from './utils'
 it('completes an item', () => {
   addTodos('first')
   toggle(0)
 })
 */
export const toggle = (k = 0) =>
  cy.window().its('model')
  .then(model => {
    expect(k, 'check item index').to.be.lessThan(model.todos.length)
    model.toggle(model.todos[k])
  })
      
      



toggle , . , "" ?





, .





context('Persistence', function () {
  // mimicking TodoMVC tests
  // by writing out this function
  function testState () {
    cy.get('.todo-list li').eq(0)
      .should('contain', TODO_ITEM_ONE).and('have.class', 'completed')
    cy.get('.todo-list li').eq(1)
      .should('contain', TODO_ITEM_TWO).and('not.have.class', 'completed')
  }

  it('should persist its data', function () {
    addTodos(TODO_ITEM_ONE, TODO_ITEM_TWO)
    toggle(0)
    .then(testState)

    .reload()
    .then(testState)
  })
})
      
      



  cy.get('.todo-list li').eq(k).find('.toggle').check() toggle(k)



. .





, , , app actions. , — , "Active" !





context('Routing', function () {
  beforeEach(addDefaultTodos) // app action

  it('should allow me to display active items', function () {
    toggle(1) // app action
    // the UI feature we are actually testing - the "Active" link
    cy.get('.filters').contains('Active').click()
    cy.get('@todos').eq(0).should('contain', TODO_ITEM_ONE)
    cy.get('@todos').eq(1).should('contain', TODO_ITEM_THREE)
  })
  // more tests
})
      
      



, , utility , , toggle, , , , , , !





// hmm, maybe we need to add a `model.toggleIndex()` method?
export const toggle = (k = 0) =>
  cy.window().its('model')
    .then(model => {
      expect(k, 'check item index').to.be.lessThan(model.todos.length)
      model.toggle(model.todos[k])
    })
      
      



model.toggleIndex



, , , . .





DRY

. app actions. , , . . app actions .   NEWTODO



TOGGLEALL



.





describe('TodoMVC', function () {
  // testing item input
  context('New Todo', function () {
    // selector to enter new todo item is private to these tests
    const NEW_TODO = '.new-todo'

    it('should allow me to add todo items', function () {
      cy.get(NEW_TODO)
        .type(TODO_ITEM_ONE)
        .type('{enter}')
      // more commands
    })
    // more tests that use NEW_TODO selector
  })

  // testing toggling all items
  context('Mark all as completed', function () {
    // selector to toggle all items is private to these tests
    const TOGGLE_ALL = '.toggle-all'

    beforeEach(addDefaultTodos)

    it('should allow me to mark all items as completed', function () {
      cy.get(TOGGLE_ALL).check()
      // more commands
    })
    // more tests that use TOGGLE_ALL selector
  })
})
      
      



  . , const NEWTODO = '.new-todo'



"New Todo", const TOGGLEALL = '.toggle-all'



"Mark all as completed". , - «» — app actions .





. , , Todo , . page objects



ALL_ITEMS



.





describe('TodoMVC', function () {
  // common selector used across many tests
  const ALL_ITEMS = '.todo-list li'

  context('New Todo', function () {
    const NEW_TODO = '.new-todo'

    it('should allow me to add todo items', function () {
      cy.get(NEW_TODO)
        .type(TODO_ITEM_ONE)
        .type('{enter}')
      cy.get(ALL_ITEMS)
        .eq(0)
        .find('label')
        .should('contain', TODO_ITEM_ONE)
    })
    // more tests
  })

  context('Mark all as completed', function () {
    const TOGGLE_ALL = '.toggle-all'

    beforeEach(addDefaultTodos)

    it('should allow me to mark all items as completed', function () {
      cy.get(TOGGLE_ALL).check()
      cy.get(ALL_ITEMS)
        .eq(0)
        .should('have.class', 'completed')
    })
    // more tests
  })
})
      
      



const ALL_ITEMS = '.todo-list li'



. utility allItems



, , .





describe('TodoMVC', function () {
  const ALL_ITEMS = '.todo-list li'

  /**
   * Returns all todo items
   */
  const allItems = () => cy.get(ALL_ITEMS)

  context('New Todo', function () {
    const NEW_TODO = '.new-todo'
    it('should allow me to add todo items', function () {
      cy.get(NEW_TODO)
        .type(TODO_ITEM_ONE)
        .type('{enter}')
      allItems()
        .eq(0)
        .find('label')
        .should('contain', TODO_ITEM_ONE)
    })
    // more tests
  })

  context('Mark all as completed', function () {
    const TOGGLE_ALL = '.toggle-all'

    beforeEach(addDefaultTodos)

    it('should allow me to mark all items as completed', function () {
      cy.get(TOGGLE_ALL).check()
      allItems()
        .eq(0)
        .should('have.class', 'completed')
    })
    // more tests
  })
})
      
      



, , spec- spec-.   .





utility allItems ALL_ITEMS allItems



specs , .





// cypress/integration/utils.js
const ALL_ITEMS = '.todo-list li'

/**
 * Returns all todo items
 * @example
    import {allItems} from './utils'
    allItems().should('not.exist')
 */
export const allItems = () => cy.get(ALL_ITEMS)

// cypress/integration/spec.js
import { allItems } from './utils'

describe('TodoMVC', function () {
  context('New Todo', function () {
    const NEW_TODO = '.new-todo'

    it('should allow me to add todo items', function () {
      cy.get(NEW_TODO)
        .type(TODO_ITEM_ONE)
        .type('{enter}')
      allItems()
        .eq(0)
        .find('label')
        .should('contain', TODO_ITEM_ONE)
    })
    // more tests
  })

  context('Mark all as completed', function () {
    const TOGGLE_ALL = '.toggle-all'

    beforeEach(addDefaultTodos)

    it('should allow me to mark all items as completed', function () {
      cy.get(TOGGLE_ALL).check()
      allItems()
        .eq(0)
        .should('have.class', 'completed')
    })
    // more tests
  })
})
      
      



, , - .





app actions . , . :





if (todos.length) {
  main = (
    <section className='main'>
      <input
        className='toggle-all'
        type='checkbox'
        onChange={this.toggleAll}
        checked={activeTodoCount === 0}
      />
      <ul className='todo-list'>{todoItems}</ul>
    </section>
  )
}
      
      



<input className='toggle-all' … />



,   « » (“Mark all as completed”) .





, .





, Todo



. .





<input
  className="toggle"
  type="checkbox"
  checked={this.props.todo.completed}
  onChange={this.props.onToggle}
/>
      
      



onChange={this.props.onToggle}



:





<input
  className='toggle'
  type='checkbox'
  checked={this.props.todo.completed}
  // onChange={this.props.onToggle}
/>
      
      



.





, . «- UI — ».





TypeScript , . , , .





App Actions





app actions , . , todos , «».





// model
app.TodoModel.prototype.addTodo = function (...todos) {
  // make XHR to the server to save todos
  ajax({
    method: 'POST',
    url: '/todos',
    data: todos
  }).then(() =>
    then update local state
    this.saveTodos(todos)
  ).then(() =>
    // this triggers DOM render
    this.inform()
  )
}
// spec.js
it('completes all items', () => {
  addDefaultTodos()
  toggle(1) // marks item completed
  // click on "Completed" link
  // assert there is 1 completed item
})
      
      







completes all items, , , , . , , .





, todos



addTodo



, toggle action



, todo 1. , , todo



— . — 1 .





, , . , . , , toggle(1)



. .





it('completes all items', () => {
  addDefaultTodos()
  allItems().should('have.length', 3)
  toggle(1) // marks item completed
  // click on "Completed" link
  // assert there is 1 completed item
})
      
      



, — app action , app action, . , Cypress DOM , .





DOM — . , POST /todos XHR



, , toggle(1)



.





it('completes all items', () => {
  cy.server()
  cy.route('POST', '/todos').as('save')
  addDefaultTodos()
  cy.wait('@save') // waits for XHR POST /todos before test continues
  toggle(1) // marks item completed
  // click on "Completed" link
  // assert there is 1 completed item
})
      
      



— ! model.inform



, , , app action.





it('completes all items', () => {
  cy.window()
    .its('model')
    .then(model => {
      cy.spy(model, 'inform').as('inform')
    })
  addDefaultTodos()
  // wait until the spy is called once
  cy.get('@inform').should('have.been.calledOnce')
  toggle(1) // marks item completed
  // click on "Completed" link
  // assert there is 1 completed item
})
      
      



Cypress UI , , .





: app actions , . , - . , . :





  1. DOM, .





  2. XHR.





  3. , .









-





. , Cypress Best Practices Brian Mann :





  • , ,





  • , , (, cy.request()), .





, cy.request



. , cy.request()



, app actions. —  .





Page Objects, , App Actions, API , .





  • . TodoMVC, Cypress Electron, 34 , 17  App Actions — 50%.





  • . , .





  • . .





, , App Actions .





export const addTodos = (...todos) => {
  cy.window().its('model').invoke('addTodo', ...todos)
}
      
      



( page objects), — , DevTools.









Application Actions. , Page Object App Actions repo bahmutov/test-todomvc-using-app-actions.





App Actions, . :





  • Dispatch Redux actions   





  • Dispatch Vuex actions DOM updates





Page Objects, App Actions .






«JavaScript QA Engineer».



« JS ».












All Articles