html-e2e
Version:
Tools for e2e functional testing through HTMl views
432 lines (378 loc) • 20.8 kB
JavaScript
const { TestUser } = require('../index')
const { expect } = require('chai')
const FakeServer = require('./FakeServer')
const expectToThrow = require('expect-to-throw')
const crypto = require('crypto');
describe('testing html view', function () {
let server, user
this.beforeEach(async function () {
server = await FakeServer()
user = await TestUser({showBrowser: false})
})
this.afterEach(async function () {
// Close the user before the server or it will hang
await user.close()
await server.close()
})
// TODO explore combinations of tests according to dimensions:
// method, type of element, asynchronous loaded, after web load, after action, case sensitive, element does not exist, exists once, exists twice, added spaces in the name
// try with all combinations and pairwise
//identifier: label, placeholder
//element type: text, textarea, pass option...
describe('mustBeAbleTo', function () {
describe('must find options', function () {
it('in buttons text', async function () {
server.setBody('<button>perform available option in button</button>')
await user.open(server.url)
await user.mustBeAbleTo('perform available option in button')
})
it('in buttons text ignoring case', async function () {
server.setBody('<button>perform AVAILABLE option in button</button>')
await user.open(server.url)
await user.mustBeAbleTo('perform available OPTION in button')
})
it('in input buttons value', async function () {
server.setBody('<input type="button" value="perform available option in button value">')
await user.open(server.url)
await user.mustBeAbleTo('perform available option in button value')
})
it('in input buttons value ignoring case', async function () {
server.setBody('<input type="button" value="perform AVAILABLE option in button value">')
await user.open(server.url)
await user.mustBeAbleTo('perform available OPTION in button value')
})
it('in buttons text filled up to 1 second after loading the webpage', async function () {
server.setBody(`
<script>setTimeout(function(){ document.getElementById('button1').innerText = 'perform available option in button'}, 1000)</script>
<button id="button1">provisional text while loading</button>
`)
await user.open(server.url)
await user.mustBeAbleTo('perform available option in button')
})
it('in buttons text filled up to 1 second after performing an action', async function () {
server.setBody(`
<button id="button0" onclick="setTimeout(function(){ document.getElementById('button1').innerText = 'perform available option in button'}, 1000)">load more actions</button>
<button id="button1">provisional text while loading</button>
`)
await user.open(server.url)
await user.doAction('load more actions')
await user.mustBeAbleTo('perform available option in button')
})
it('in input buttons text filled up to 1 second after loading the webpage', async function () {
server.setBody(`
<script>setTimeout(function(){ document.getElementById('button1').value = 'perform available option in input button'}, 1000)</script>
<input type="button" id="button1" value="provisional text while loading">
`)
await user.open(server.url)
await user.mustBeAbleTo('perform available option in input button')
})
it('in input buttons text filled up to 1 second after performing an action', async function () {
server.setBody(`
<button id="button0" onclick="setTimeout(function(){ document.getElementById('button1').value = 'perform available option in input button'}, 1000)">load more actions</button>
<input type="button" id="button1" value="provisional text while loading">
`)
await user.open(server.url)
await user.doAction('load more actions')
await user.mustBeAbleTo('perform available option in input button')
})
})
describe('must throw', function () {
describe('when the option is not available because', function () {
it('does not exist', async function () {
await user.open(server.url)
await expectToThrow('user is not able to perform not existing option', async function () {
await user.mustBeAbleTo('perform not existing option')
})
})
it('exists as input but is disabled', async function () {
server.setBody('<input type="button" value="perform disabled option as input" disabled>')
await user.open(server.url)
await expectToThrow('user is not able to perform disabled option as input', async function () {
await user.mustBeAbleTo('perform disabled option as input')
})
})
it('exists as button but is disabled', async function () {
server.setBody('<button disabled>perform disabled option as button</button>')
await user.open(server.url)
await expectToThrow('user is not able to perform disabled option as button', async function () {
await user.mustBeAbleTo('perform disabled option as button')
})
})
})
})
})
//TODO: add support for progress
//TODO: add support for tables
function generateInputWithLabel({type, label='anyLabel:', value='anyValue', inputId='any_id', labelFor=inputId, valueAfterLoading, loadTime}){
let labelhtml = `<label for="${labelFor}">${label}</label>`
let inputElement = generateInput({type, value, id:inputId, valueAfterLoading, loadTime})
return `${labelhtml}${inputElement}`
}
function generateInput({type, value, id, placeholder, valueAfterLoading, loadTime}){
let placeholderHtml = placeholder ? `placeholder="${placeholder}"` : ''
let _id = id ?? crypto.randomUUID()
let script = loadTime ? `<script>setTimeout(function(){ document.getElementById('${_id}').value = '${valueAfterLoading}'}, ${loadTime})</script>` : ''
if (type == 'textarea') return `${script}<textarea id="${_id}" ${placeholderHtml}>${value}</textarea>`
if (type == 'text') return `${script}<input type="text" id="${_id}" value="${value}" ${placeholderHtml}>`
if (type == 'password') return `${script}<input type="password" id="${_id}" value="${value}" ${placeholderHtml}>`
throw new Error(`Unsupported type ${type}`)
}
//TODO: get all the data in the test from the testCase and make a testCase generator to generate all of them according to some testing rules (like textarea has no placeholder)
const identifierTestCases = [
{condition: '', labelName:'age', property: 'age'},
{condition: 'ignoring case', labelName:'Age', property: 'agE'},
{condition: 'ignoring spaces', labelName:' age ', property: 'age'},
{condition: 'with several words', labelName:' Date of birth ', property: 'date of birth'},
]
const labelTestCases = [
...identifierTestCases,
{condition: 'ignoring colon', labelName:' Age: ', property: 'age'},
]
const placeholderTestCases = [
...identifierTestCases,
{condition: 'ignoring dots', labelName:' Age... ', property: 'age'},
]
for(let elementType of [
'text',
'textarea',
'password'
]){
describe('get', function () {
describe(`when the identifier of ${elementType} element is in a label`, function () {
for(let labelTestCase of labelTestCases){
it(`must return the input value ${labelTestCase.condition}`, async function () {
await server.setBody(generateInputWithLabel({type: elementType, label: labelTestCase.labelName, value: '18'}))
await user.open(server.url)
const age = await user.get(labelTestCase.property)
expect(age).to.equal('18')
})
}
it('must throw when the label includes the property but is not the entire word', async function () {
await server.setBody(generateInputWithLabel({type: elementType, label: 'Marriage:', value: '1987-01-23'}))
await user.open(server.url)
await expectToThrow('property "age" not found', async function () {
await user.get('age')
})
})
it('must throw when the field related to the label does not exist', async function () {
await server.setBody(generateInputWithLabel({type: elementType, label: 'Age:', inputId: 'age', labelFor: 'notexistingField'}))
await user.open(server.url)
await expectToThrow('missing input field for label "Age:"', async function () {
await user.get('age')
})
})
it(`must wait until no progress tag is present`, async function () {
await server.setBody(`
${getProgressTag({timeToDisappear:2000})}
${generateInputWithLabel({type: elementType, label: 'anylabel', value: '5', valueAfterLoading: '18', loadTime: 1800})}`)
await user.open(server.url)
const age = await user.get('anylabel')
expect(age).to.equal('18')
})
})
describe(`when the identifier of ${elementType} element is in a placeholder`, function () {
for(let placeholderTestCase of placeholderTestCases){
it(`must return the input value ${placeholderTestCase.condition}`, async function () {
await server.setBody(generateInput({type: elementType, placeholder: placeholderTestCase.labelName, value: '18'}))
await user.open(server.url)
const age = await user.get(placeholderTestCase.property)
expect(age).to.equal('18')
})
}
it(`must return the input value when there is another ${elementType} element without placeholder`, async function () {
let body = generateInput({type: elementType}) +
generateInput({type: elementType, placeholder: 'age', value: '18'})
await server.setBody(body)
await user.open(server.url)
const age = await user.get('age')
expect(age).to.equal('18')
})
})
})
describe('set', function () {
describe(`when the identifier of ${elementType} element is in a label`, function () {
for(let labelTestCase of labelTestCases){
it(`must set the value in the input ${labelTestCase.condition}`, async function () {
await server.setBody(generateInputWithLabel({type: elementType, label: labelTestCase.labelName, value:'oldvalue'}))
await user.open(server.url)
await user.set(labelTestCase.property, '18')
const age = await user.get(labelTestCase.property)
expect(age).to.equal('18')
})
}
it('must throw when the label includes the property but is not the entire word', async function () {
await server.setBody(generateInputWithLabel({type: elementType, label: ' Marriage: ', value:'oldvalue'}))
await user.open(server.url)
await expectToThrow('property "age" not found', async function () {
await user.set('age', 'newValue')
})
})
it('must throw when the field related to the label does not exist', async function () {
await server.setBody(generateInputWithLabel({type: elementType, label: 'Age:', inputId: 'age', labelFor: 'notexistingField', value: 'oldvalue'}))
await user.open(server.url)
await expectToThrow('missing input field for label "Age:"', async function () {
await user.set('age', 'newValue')
})
})
})
describe(`when the identifier of ${elementType} element is in a placeholder`, function () {
for(let placeholderTestCase of placeholderTestCases){
it(`must set the value ${placeholderTestCase.condition}`, async function () {
await server.setBody(generateInput({type: elementType, placeholder: placeholderTestCase.labelName, value: 'oldvalue'}))
await user.open(server.url)
await user.set(placeholderTestCase.property, '18')
const age = await user.get(placeholderTestCase.property)
expect(age).to.equal('18')
})
}
})
it(`must wait until no progress tag is present`, async function () {
await server.setBody(`
${getProgressTag({timeToDisappear:2000})}
${generateInputWithLabel({type: elementType, label: 'anylabel', value: '5', valueAfterLoading: '18', loadTime: 2000})}`)
await user.open(server.url)
await user.set('anylabel', 'myValue')
const value = await user.get('anylabel')
expect(value).to.equal('myValue')
})
})
}
function getActionElementWithResult({type, text, ariaLabel, id, disabled, title}){
return `${getResultFieldWithLabel()}${getActionElementToWriteOnResult({type, text, ariaLabel, id, disabled, title})}`
}
function getActionElementToWriteOnResult({type, text, ariaLabel, id, disabled, title}){
let ariaLabelHtml = ariaLabel ? `aria-label="${ariaLabel}"` : ''
let _id = id ?? crypto.randomUUID()
let disabledHtml = disabled ? 'disabled' : ''
let titleHtml = title ? `title="${title}"` : ''
switch (type){
case 'button': return `<button id="${_id}" ${ariaLabelHtml} onclick="document.getElementById('result').value = 'clicked'" ${titleHtml} ${disabledHtml}>${text}</button>`
case 'input button': return `<input type="button" id="${_id}" ${ariaLabelHtml} onclick="document.getElementById('result').value = 'clicked'" value="${text}" ${titleHtml} ${disabledHtml}>`
case 'input submit': return `<input type="submit" id="${_id}" ${ariaLabelHtml} onclick="document.getElementById('result').value = 'clicked'" value="${text}" ${titleHtml} ${disabledHtml}>`
case 'link': return `<a href="#" id="${_id}" ${ariaLabelHtml} onclick="document.getElementById('result').value = 'clicked'" ${titleHtml} ${disabledHtml}>${text}</a>`
default: throw new Error(`Unsupported type ${type}`)
}
}
function getResultFieldWithLabel(){
return '<label for="result">Result</label><input type="text" id="result">'
}
function getElementTypeTextProperty(type){
//TODO: I think these should be different values of the same 'text' property of a class for each element type
switch (type){
case 'button': return 'innerText'
case 'input button': return 'value'
case 'input submit': return 'value'
case 'link': return 'innerText'
default: throw new Error(`There is no text property defined for type ${type}`)
}
}
function getProgressTag({timeToDisappear}){
let id = crypto.randomUUID()
return `
<script>setTimeout(function(){ document.getElementById('${id}').remove()}, ${timeToDisappear})</script>
<progress id="${id}"></progress>`
}
describe('doAction', function () {
for(let elementType of [
'button',
'input button',
'input submit',
'link'
]){
describe(`when the action is a ${elementType}`, function () {
it(`must click the ${elementType}`, async function () {
await server.setBody(getActionElementWithResult({type: elementType, text: 'perform action'}))
await user.open(server.url)
await user.doAction('perform action')
const clicked = await user.get('result')
expect(clicked).to.equal('clicked')
})
for(let idTestCase of identifierTestCases){
it(`must click the ${elementType} ${idTestCase.condition}`, async function () {
await server.setBody(getActionElementWithResult({type: elementType, text: idTestCase.labelName}))
await user.open(server.url)
await user.doAction(idTestCase.property)
const clicked = await user.get('result')
expect(clicked).to.equal('clicked')
})
}
// TODO: There is a problem with the "aria-label" because its value is not visible in the browsers
// If the button isn't entirely legible because it's using a symbol, we have the "title" property to better describe it
// Or if we add an image, we have the "alt" attribute
// Both are visible to screen readers as well as users who can see
// They say that "title" isn't read by all screen readers; I understand that's their issue and they should improve their implementation
// It's also true that even in the HTML standard it's recommended not to use "title" for this reason, which seems wrong to me
// Screen readers and browsers should adapt to the standard and not the other way around
// And the standard should adapt to the users; to me, the correct approach would be for either "aria-label" to be displayed in all agents
// Or for "title" to be displayed in all agents. Meanwhile, I'll suggest using "title" or "alt" as it gives the highest chance of being understood
// We can also request that if there is a "title", there should also be an "aria-label" with the same content
//or while HTML standard solves their problems we could not recognize any of those. Either you set the text or you use an img with "alt".
for(let idTestCase of identifierTestCases){
it(`must click the ${elementType} based on the title attribute when the text is not readable ${idTestCase.condition}`, async function () {
await server.setBody(getActionElementWithResult({type: elementType, text: '>', title: idTestCase.labelName}))
await user.open(server.url)
await user.doAction(idTestCase.property)
const clicked = await user.get('result')
expect(clicked).to.equal('clicked')
})
}
it(`must click ${elementType} which text is filled up to 1 second after loading the webpage`, async function () {
let htmlBody = `
<script>setTimeout(function(){ document.getElementById('action1').${getElementTypeTextProperty(elementType)} = 'perform action'}, 1000)</script>
${getActionElementWithResult({
type: elementType,
text: 'privisional text while loading',
id: 'action1'})}
`
await server.setBody(htmlBody)
await user.open(server.url)
await user.doAction('perform action')
const clicked = await user.get('result')
expect(clicked).to.equal('clicked')
})
it(`must click the ${elementType} which text is filled up to 1 second after performing an action`, async function () {
await server.setBody(`
<button id="button0" onclick="setTimeout(function(){ document.getElementById('action1').${getElementTypeTextProperty(elementType)} = 'perform action'}, 1000)">load more actions</button>
${getActionElementWithResult({
type: elementType,
text: 'privisional text while loading',
id: 'action1'})}
`)
await user.open(server.url)
await user.doAction('load more actions')
await user.doAction('perform action')
const clicked = await user.get('result')
expect(clicked).to.equal('clicked')
})
it(`must throw when the ${elementType} is disabled`, async function () {
let htmlBody = getActionElementWithResult({type: elementType, text: 'perform action', disabled: true})
await server.setBody(htmlBody)
await user.open(server.url)
await expectToThrow('user could not perform action', async function () {
await user.doAction('perform action')
})
})
})
}
it('must wait until no progress tag is visible', async function(){
await server.setBody(`
${getProgressTag({timeToDisappear:2000})}
<input type="text" id="click-counter" placeholder="click count" value="0"/>
<input type="text" id="last-click-moment" placeholder="last click moment" value=""/>
<button onclick="
document.getElementById('click-counter').value = parseInt(document.getElementById('click-counter').value) + 1;
document.getElementById('last-click-moment').value = Date.now();
">perform action</button>
`)
const startTime = Date.now()
await user.open(server.url)
await user.doAction('perform action')
const clickCount = await user.get('click count')
const lastClickMomentNumber = await user.get('last click moment')
const lastClickMoment = parseInt(lastClickMomentNumber)
const timeToClick = lastClickMoment - startTime
expect(clickCount).to.equal('1')
expect(timeToClick).to.be.greaterThanOrEqual(2000)
})
})
})