-
Notifications
You must be signed in to change notification settings - Fork 2
Writing tests
You can see the abstract testing algorithm in the code block below.
for (test in tests) {
result = invoke(test)
if (not result.correct) {
break
}
}
As you can see, there are some methods that are invoked and return a result. The first failed test stops the whole testing. Let's see how to implement these methods.
All of the classes and methods we will discuss below can be imported from the hs-test-web
module:
const {StageTest, ReactTest, correct, wrong} = require("hs-test-web")
Every hs-test-web
test should one way or another extend StageTest
class (or extend another class that extends this one).
Example:
class VirtualPianoTest extends StageTest { }
StageTest
class has tests
field that represent an array of test cases. Each test case is a function that will be executed in the browser or Node.js
context. You will know more about contexts in the following sections.
class Test extends StageTest {
tests = [
// test cases should be declared here
]
}
This class is used to test React projects. The test class for the React
project should extend ReactTest
class.
The features of testing React projects are described in the section React Project
Example:
class MineSweeperTest extends ReactTest { }
StageTest
class has method getPage(url)
that accepts an website URL
or path to an HTML file and returns an instance of a Page
class. Each instance of the Page
class represents the browser tab, where the test cases can be executed.
All the Page
class instances should be declared as fields of the class that extends StageTest
class:
class Test extends StageTest {
page = this.getPage(url)
}
Page
class instances will be used to run test cases in the browser context or you will need them to interact with HTML elements in Node.js
context. You will learn more about contexts in the following sections.
Page
class objects have the following fields:
-
tests
- array of test cases. -
requests
- array of catched request from the page. Each element of array is puppeteer.HTTPRequest object.
Page
clas objects have the following methods to work with browser tabs:
-
async navigate(url)
- navigates to theurl
. -
async refresh()
- refreshes the page and waits untildomcontentloaded
event. -
url()
- returns current URL of the page. -
async waitForEvent(eventName, timeout)
- returnsPromise
that resolves into a boolean value, that describes if the event is emitted on the page or not. Learn more about the method in the Event handling section.
The browser tab is opened when one of the following methods is called for the first time:
-
execute(func)
- is used to declare test case intests
field -
evaluate(func)
- is used to executefunc
function on the page from theNode.js
context - find methods - are used to find HTML elements from the
Node.js
context. You will learn about these methods in theNode.js
context section
CheckResult
object represents the result of the test. In case of a failed test, a special message will be shown to the user that should be specified in tests. This message is called feedback
. It should be understood that the correct()
method is a short form of CheckResult.correct()
. The same for wrong()
method. Both methods can be imported from the hs-test-web
module const {correct, wrong} = require("hs-test-web")
If a user's program failed the test feedback should be shown to the user to help to resolve their problem or to find a mistake. Please, do not write feedbacks like Wrong answer
or Try again
- such feedbacks are useless for the user. For example, instead of Your answer is wrong
you can help the user by providing clearer feedback: Cannot find element with class 'container'
. See the difference: in this case, it becomes obvious for the user that they may be somehow missed class name of an HTML element. Remember that Hyperskill
is a learning platform and feedbacks of tests must help the user to spot the mistake.
Example:
const {StageTest, correct, wrong} = require("hs-test-web")
const pagePath = 'file://' + path.resolve(__dirname, './index.html');
class Test extends StageTest {
page = this.getPage(pagePath)
tests = [
this.page.execute(() => {
let containerElements = document.getElementsByClassName('container');
if (containerElements.length === 0) {
return wrong(`Cannot find element with class 'container'`);
} else if (containerElements.length > 1) {
return wrong(`Found ${containerElements.length} elements with class 'container'` +
`, the page should contain just a single such element.`);
}
return correct()
})
]
}
Node.js context |
---|
These exceptions are very useful if you want to finish testing being deep into several methods. It can be useful if you are under a lot of methods and don't want to continue checking everything else.
Throwing WrongAnswer
exception will be treated like returning CheckResult.wrong(...)
object. It can be used often. It is recommended to use this class everywhere instead of CheckResult.wrong(...)
Throwing TestPassed
exception will be treated like returning CheckResult.correct()
object. It is used rarely.
Examples:
throw new WrongAnswer("Count should be 42, found " + count);
throw new TestPassed();
There are two types of the context where the test cases are executed.
The test cases are executed inside of the browser on the chosen page. You can imagine that you are running code in the DevTools console. To run the test case on a page you should call its execute()
method that accepts the arrow function. Inside the arrow function, you should write a code that will be executed on the page:
const {StageTest, correct, wrong} = require('hs-test-web');
class Test extends StageTest {
mainPage = this.getPage(URL)
tests = [
this.mainPage.execute(() => {
// this code will be executed on the 'mainPage' page.
const element = document.getElementById('wrapper')
return correct()
})
]
}
We recommend using the browser context to check elements for their existence, styles, and attributes. Because when interacting with HTML elements, e.g. when you click on them, the page may reload or redirect to another page. In this case, page context will be destroyed and further code execution will be impossible, which will lead to errors. To avoid such challenges we recommend you to use Node.js
context (environment) to interact with HTML elements.
In the Node.js
context you can interact with a page object itself. You can reload the page, navigate between web pages, and so on. To execute test case in the Node.js environment you should use execute()
method of the node
field that comes with StageTest
class:
class Test extends StageTest {
tests = [
this.node.execute(async () => {
// this code will be executed in the Node.js context
const inputField = await this.page.findById('input-text')
await inputField.inputText('test text content!')
const updateTextButton = await this.page.findById('update-text')
await updateTextButton.click()
const textDiv = await this.page.findById('text-from-input')
if (await textDiv.textContent() !== 'test text content!') {
return wrong('wrong text content!')
}
return correct()
})
]
}
class Test extends StageTest {
page = this.getPage(pagePath)
tests = [
this.node.execute(() => {
return correct()
}),
this.page.execute(() => {
return correct()
})
]
}
Sometimes you might need to get some data from the browser context and process it in the Node.js
using special modules. It can be done by calling evaluate()
execute method of the needed Page
object:
class Test extends StageTest {
page = this.getPage(pagePath)
tests = [
this.page.execute(() => {
this.getPixels = () => {
const canvas = document.getElementsByTagName("canvas")[0];
if (canvas.width !== 30 || canvas.height !== 30) {
return wrong("After uploading an image into canvas it has wrong size!")
}
const ctx = canvas.getContext("2d");
return ctx.getImageData(0, 0, canvas.width, canvas.height).data;
}
return correct()
}),
this.node.execute(() => {
let userPixels = await page.evaluate(() => {
return this.getPixels()
});
let realPixels = await pixels(brightnessTestImage)
let compareResult = comparePixels(userPixels, realPixels.data);
if (!compareResult) {
return wrong("After increasing brightness of the image it has wrong pixel values!")
}
return correct()
})
]
}
It is possible that the required data may not exist in the context of the browser. In this case you can return wrong(message)
and the test in Node.js
will fail with feedback message:
this.node.execute(async () => {
await this.page.evaluate(async () => {
return wrong('wrong result!') // will fail the test
})
return correct()
})
Also you can pass the test by returning correct()
from the browser context:
this.node.execute(async () => {
await this.page.evaluate(async () => {
return correct() // will pass the test
})
return wrong('wrong result!')
})
From the puppeteer documentation you can learn how to pass arguments to the evaluate
method.
There are several asynchronous functions that will help you to find HTML elements on the page:
-
async findById(id)
- finds element by itsID
. If such an element doesn't exist thennull
will be returned -
async findByClassName(className)
- finds the first element withclassName
class. If such an element doesn't exist thennull
will be returned -
async findBySelector(selector)
- finds the first element usingselector
selector. If such an element doesn't exist thennull
will be returned -
async findAllByClassName(className)
- finds all elements withclassName
class. If any element wasn't found then an empty array is returned[]
-
async findAllBySelector(selector)
- finds all elements usingselector
selector. If any element wasn't found then an empty array is returned[]
You should await executions for every function using await
keyword.
Each HTML element in the Node.js
context represents an object of the Element
class that has all functions described above, so you can HTML elements inside of an HTML element. When you call find*()
methods from Page
class object, the search happens inside the <body>
tag.
class TestFindElements extends StageTest {
page = this.getPage(pagePath)
tests = [
this.node.execute(async () => {
const test1 = await this.page.findAllByClassName('t1')
const test2 = await this.page.findAllBySelector('div.t1')
const test3 = await this.page.findByClassName('test')
const another = await test3.findByClassName('another')
const t2s1 = await another.findAllByClassName('t2')
if (t2s1.length !== 3) {
return wrong("wrong!")
}
const t2s2 = await another.findAllBySelector('div.t2')
if (t2s2.length !== 3) {
return wrong("wrong!")
}
return correct()
})
]
}
Element class has several methods to get an HTML element attributes:
-
async textContent()
- returns text content of an HTML element -
async innerHtml()
- returns inner HTML of an HTML element -
async className()
- returns the class attribute of an HTML element -
async getProperty(property)
- returns an property of an HTML element by itsproperty
name -
async getAttribute(attribute)
- returns an attribute of an HTML element by itsattribute
name -
async getStyle()
- returns object with styles that applied to and HTML element -
async getComputedStyle()
- returns object of the computed styles of an HTML element
The values of specific styles can be accessed from the object fields:
const containerStyles = await container.getStyles()
if (containerStyles.backgroundColor !== 'red') {
return wrong('wrong backgroundColor')
}
const containerComputedStyles = await container.getComputedStyles()
if (containerComputedStyles.backgroundColor !== 'rgb(255, 0, 0)') {
return wrong('wrong backgroundColor')
}
The following methods allow you to interact with HTML elements:
-
async click()
- simulates a mouse click on an element -
async type(text)
- focuses on the element and simulates the input oftext
from the keyboard -
async focus()
- focuses on the element -
async hover()
- hover the element -
async waitForEvent(eventName, timeout)
- returnsPromise
that resolves into a boolean value, that describes if the event is emitted on the element or not. Learn more about the method in the Event handling section.
To handle JS events you can use waitForEvent(eventName, timeout)
function, where eventName
- is the name of the event to wait for, and timeout
is a timeout of waiting for the event.
The waitForEvent
function returns a Promise
that resolves into boolean value: true
- if the event is emitted within timeout
ms, otherwise false
.
Keep in mind that you mustn't await
for the waitForEvent
function, otherwise, it will stop executing code and return false
after timeout
ms. Instead, save the pending Promise
into a variable, interact with the elements that are expected to emit events, only then await
for Promise
to resolve. According to the Promise
resolved value make a conclusion if the expected event had been emitted or not. Example:
class Test extends StageTest {
page = this.getPage(pagePath)
tests = [
this.node.execute(async () => {
const button = await this.page.findById("test-button");
const isEventHappened = button.waitForEvent("click", 2000);
await button.click();
return (await isEventHappened === true) ? correct() : wrong("Expected click event on button with 'test-button' id!");
})
]
}
If the waitForEvent
function is called from the Element
object then the event is handled for that element. If the function is called from the Page
object then the event is handled for the window
object.
The test library has the ability to open multiple pages during tests. To do this, create several Page
objects and call the execute()
method for each page:
class Test extends StageTest {
page1 = this.getPage(page1Url)
page2 = this.getPage(page2Url)
tests = [
this.page1.execute(() => {
return correct()
}),
this.page2.execute(() => {
return correct()
})
]
}
It's important to understand that the page opens only when the execute()
method is invoked on that page for the first time. From the example above, page1 is opened, then the test case is executed in the context of that page, then page2 is opened, then the test case is executed in the context of that page, then all pages are closed and the tests are terminated.