Hello, Khabrovites. For future students of the JavaScript QA Engineer course we prepared a translation of useful material.
We also invite everyone to take part in the open webinar on the topic “What a tester needs to know about JS”. The lesson will consider the features of JS, which you need to keep in mind all the time when writing tests.
— . -, 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
, .
! 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
})
:
UI — , "New Todo".
, .
— - . " " (“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
})
})
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 , . , - . , . :
DOM, .
XHR.
, .
-
. , 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 Vuex actions DOM updates
Page Objects, App Actions .
«JavaScript QA Engineer».
« JS ».