REserve — Testing UI5 — Building a platform
In this first article, we setup the runner by building a configurable platform that serves the web application and offers basic services.
Defining and configuring a job
To centralize the different parameters of the runner, a job object is defined with a list of properties.
const job = {
cwd: process.cwd(),
port: 0,
ui5: 'https://ui5.sap.com/1.87.0',
webapp: 'webapp',
logServer: false
}
Job definition with named parameters
The list of parameters will be changed while covering the different steps but we begin with :
cwd
: the current working directory, it is initialized with the process current oneport
: the port used to serve the application (0
means REserve will allocate one)ui5
: the base URL of the content delivery network to grab UI5 fromwebapp
: the webapp directory of the application to servelogServer
: enables REserve logs
A very basic parameter parsing offers the possibility to alter some of these settings through the command line (using syntax -<parameter>:<value>
).
process.argv.forEach(arg => {
const valueParsers = {
boolean: value => value === 'true',
number: value => parseInt(value, 10),
default: value => value
} const parsed = /-(\w+):(.*)/.exec(arg)
if (parsed) {
const [, name, value] = parsed
if (Object.prototype.hasOwnProperty.call(job, name)) {
const valueParser = valueParsers[typeof job[name]] || valueParsers.default
job[name] = valueParser(value)
}
}
})
Basic parsing enabling parameter setting from the command line
Last but not least, all the parameters that relate to paths are made absolute to simplify file handling.
const { isAbsolute, join } = require('path')function toAbsolute (member, from = job.cwd) {
if (!isAbsolute(job[member])) {
job[member] = join(from, job[member])
}
}toAbsolute('cwd', process.cwd())
toAbsolute('webapp')
Making all path-like parameters absolute
Serving the application
To serve the tested web application, REserve is embedded in the runner by importing the relevant functions and exposing two kind of mappings :
- UI5 resources
- The project sources
Once the server started, the port is stored at the job level (in the event REserve allocated it).
const { join } = require('path')
const ui5 = require('./src/ui5')
const { check, log, serve } = require('reserve')const job = require('./src/job')async function main () {
const configuration = await check({
port: job.port,
mappings: [
...ui5, {
// Project mapping
match: /^\/(.*)/,
file: join(job.webapp, '$1')
}]
})
const server = serve(configuration)
if (job.logServer) {
log(server)
}
server
.on('ready', async ({ url, port }) => {
job.port = port
if (!job.logServer) {
console.log(`Server running at ${url}`)
}
})
}main()
Regarding ui5 resources, this first version simply proxifies the UI5 content delivery repository using REserve’s url
handler.
For performance reasons and to support additional resources such as libraries, the UI5 mappings will become more complex. This will be explained in a separate article and it elucidates why the definition is isolated.
'use strict'const job = require('./job')const mappings = [{
// UI5 from url
method: ['GET', 'HEAD'],
match: /\/((?:test-)?resources\/.*)/,
url: `${job.ui5}/$1`
}]module.exports = mappings
UI5 mapping
So far, we have a functional and configurable web server capable of serving UI5 applications. You may download the consolidated source and play with it (don’t forget to install REserve).
Spawning a browser
In order to execute the tests, the runner requires the ability to instantiate browsers. Furthermore, it will execute several ones to parallelize the tests. It means that we need a way to identify which browser executes which test.
The browser execution is delegated to a separate Node.js script. It means that the runner will expose two new parameters :
browser
: the path of a Node.js script that is responsible of starting the browser. By default, a script leveraging puppeteer is provided.args
: parameters for the script. Two tokens can be used to inject specific values :__URL__
: contains the URL of the test page to execute__REPORT__
: contains the path to a folder where the script can save additional information related to the test execution (such as console logs or screenshots)
Two new APIs exposes the service internally :
start (relativeUrl)
stop (relativeURl)
The first one executes the script and keep track of the forked process. The second one sends a message to the process to request its end. The start
API returns a Promise that is resolved when the corresponding stop
API has been called.
It means that the test page URL is used as a key to identify the browsers. Whenever the browser triggers an endpoint exposed by the platform, it reads the referer header to capture the URL and identify the browser / test being executed.
This service is implemented in the browsers
module
Defining an endpoint
During the tests execution, the runner needs endpoints to receive feedback from the spawned browsers.
As a lazy developer, I don’t want to repeat myself (DRY concept). Hence a function is used to generate these endpoints : it immediately acknowledges the response, captures the URL of the browser by reading the referer header and deserializes the request body.
The endpoint factory takes a callback (named implementation
) that is called with the browser url and the received JSON data.
const { body } = require('reserve')function endpoint (implementation) {
return async function (request, response) {
response.writeHead(200)
response.end()
const [, url] = request.headers.referer.match(/http:\/\/[^/]+(?::\d+)?(\/.*)/)
const data = JSON.parse(await body(request))
try {
await implementation.call(this, url, data)
} catch (e) {
console.error(`Exception when processing ${url}`)
console.error(data)
console.error(e)
}
}
}
Endpoint generation helper
Next step
The platform is now ready to execute the tests. The next step is to extract them.