@tremho/jove-test
Version:
Test API Module for Jove Framework
229 lines (203 loc) • 8.47 kB
text/typescript
import Tap from 'tap'
import {WSServer,setEndResolver} from "./WSServer"
import path from "path";
import fs from "fs";
import {compareToComp} from "./imageComp";
let stream:WSServer
let desc: string, r:any, x: any
let runcount = 0
let previous:Promise<unknown>
let prevResolve:any
/**
* Transact a single remote test action at the connected app client
*
* @param {TAP} t The tap instance passed into test execution function from `runRemoteTest`, or null to skip tap
* @param {string} action The directive to perform
* @param {string} description A description of this test action
* @param {string} expected The expected return result of this test action
*
* @return {boolean} the result of the test result matching expected; may be ignored.
*/
export async function testRemote(t:any, action:string, description:string, expected?:any) {
desc = description
x = expected
r = await stream.sendDirective(action)
if(typeof r === 'string' && typeof x !== 'string') x = ''+x
const ok = r === x
if(t) t.ok(ok, desc + ` expected ${x}, got ${r}`)
return ok
}
/**
* similar to `testRemote`, but simply calls the action and returns the result without submitting to test
*
* @param {string} action The directive to perform
* @returns {string} The JSON result of the action
*/
export async function callRemote(action:string) {
// console.log('callRemote', action)
let r = await stream.sendDirective(action)
if(typeof r === 'string') {
try {
r = JSON.parse(r)
} catch(e) {}
}
return r
}
/**
* Should be called at the top of a test suite, but it's just for symmetry with `endTest`
* In this version, nothing is actually sent to the server.
* @param {TAP} t The tap instance, if using tap
*
* return {boolean} true if stream is ready.
*/
export async function startTest(t:any = null) {
// console.log("%%%%%%%%%%%%%% startTest directive called %%%%%%%%%%%%%%%")
desc = 'stream connect'
r = !!stream
x = true
const ok = r === x
if(t) t.ok(ok, desc + (ok ? ' successful': ' FAILED'))
return ok
}
/**
* Call when all the tests are complete. The client will continue with its disposition after that, either exiting, or
* continuing to run. This should be the end of any remote testing, regardless.
* @param {TAP} t The tap instance, if using tap. Will signal the end on this tap instance.
*/
export async function endTest(t:any = null) {
// console.log('endTest called', prevResolve)
if(t) t.end()
let report:any = await stream.sendDirective('getReport')
report = report.replace(/--/g, '=')
saveReport(report)
return stream.sendDirective('end')
}
/**
* Initiate the connected test. Pass the title of the test and the async function that conducts the test suite
*
* At this point the client has been launched, possibly under appium
*
* @param {string} title Title of this test that will appear on the report
* @param {Function} testFunc The function from the test script that conducts the test with `startTest` then a series of `testRemote` directives, then an `endTest`
*/
export async function runRemoteTest(title:string, testFunc:any) {
stream = new WSServer()
let cf = await stream.listen()
setEndResolver(() => {
process.exit(0)
})
await stream.sendDirective('startReport '+title)
return Tap.test('Remote E2E: '+title, (t:any) => {
if(cf) testFunc(t)
else {
t.ok(false, 'Only one Remote Test in a test suite is allowed.')
}
})
}
/**
* Takes a screenshot of the current page
*
* Image will be saved as a PNG in the appropriate report directory
*
* @param {string} name Name to give this image
*/
export async function screenshot(t:any, name:string) {
// console.log('jove-test is issuing a screenshot call...')
const ssrt:any = await stream.sendDirective('screenshot '+name)
if(ssrt.substring(0,4) === 'data') {
// console.log('we see a base 64 return of', ssrt.substring(0,10)+'...', 'that we could write to a file for', name)
const rootPath = path.resolve('.')
if(fs.existsSync(path.join(rootPath, 'report', 'latest'))) {
const rptImgPath = path.join(rootPath, 'report', 'latest', 'images')
fs.mkdirSync(rptImgPath, {recursive: true})
const imgPath = path.join(rptImgPath, name + '.png')
const b64 = ssrt.substring(ssrt.indexOf(',') + 1)
fs.writeFileSync(imgPath, b64, "base64")
t.ok(true, 'screenshot '+name+' taken')
// console.log('image saved as', fs.realpathSync(imgPath))
// console.log('verified: ', fs.existsSync(imgPath))
return imgPath
} else {
t.ok(false, 'screenshot '+name+' fails - invalid root directory')
console.error('rootPath is not recognized', rootPath)
return "ERR:Bad-rootPath"
}
}
t.ok(false, 'screenshot '+name+' fails - data return not recognized')
console.error('data return not recognized', ssrt.substring(0,5))
return "ERR:Not-Base64"
}
/**
* Compare a screenshot taken with `screenshot` to a comp file
* in the `reports/comp` directory of the same name
* @param {TAP} t the Tap object to report through , or null if not using
* @param {string} name Name of the screenshot / comp image
* @param [passingPct] Percentage of pixels that can be different and still pass (default = 0)
*/
export async function compare(t:any, name:string, passingPct= 0) {
// console.log('test: compare --->>')
const data:any = await compareToComp(name+".png", passingPct)
// console.log('data returned', data)
let ok = data && data.ok
let message = (ok ? 'image matches' : data.error || 'image does not match'+ ` (${data.percentDiff}% difference)`)
if(t) t.ok(ok, 'compare '+name+': '+message)
if(!ok) {
let res = `${name},${data.percentDiff},${data.error || ''}`
await callRemote('compareReport ' + res)
}
return data
}
/**
* Assigns a title for this test that will appear on test report.
* @param {TAP} [t] The TAP object, if using and want report on this operation
* @param {string} title Title of this test series
*/
export async function remoteTitle(t:any, title:string) {
await callRemote('remoteTitle '+title.replace(/ /g, '+'))
if(t) {
t.ok(true, '>remoteTitle: '+title)
}
}
/**
* Prsent a prompt dialog to the user asking a question with given button choices, and the expected response for all OK.
* Test passes if response is the expected response, or if there is a timeout.
* If no response is made within the timeout period (30 seconds default), the dialog is dismissed and the test passses.
*
* @param {TAP} t The TAP object
* @param {string} prompt prompt to the user
* @param {string} choices comma-delimited set of choice buttons to present
* @param {string} expect the response expected for passing
* @param {number} [timeoutSeconds] Seconds until timeout (default is 30)
*/
export async function askAHuman(t:any, prompt:string, choices:string, expect:string, timeoutSeconds=30) {
console.log(">>> Ask a Human")
let px = prompt.replace(/\+/g, '%plus%')
px = px.replace(/ /g, '+')
let resp = await callRemote('askAHuman '+px+ ' '+choices +' '+ expect+' '+timeoutSeconds)
console.log(' response is ', resp)
if(resp === 'undefined') resp = undefined;
let exprt = resp ? (resp === expect) ? ` [${resp}]` : ` [${resp}, expected ${expect}]`
: ` [timed out]`
t.ok(resp===expect || resp===undefined, 'askAHuman: '+prompt+exprt)
}
/**
* Waits for the given time, in milliseconds
* @param millis
*/
export async function wait(millis:number) {
return new Promise(resolve => {setTimeout(resolve, millis)})
}
function saveReport(report:string) {
const rootPath = path.resolve('.')
// console.log("TEST REPORT ROOT PATH", rootPath)
if(fs.existsSync(path.join(rootPath, 'package.json'))) {
const dtf = "current"
const folderPath = path.join(rootPath, 'report', 'latest')
// fs.mkdirSync(folderPath, {recursive:true})
const rptPath = path.join(folderPath, 'report.html')
// console.log("TEST REPORT PATH", rptPath)
fs.writeFileSync(rptPath, report)
} else {
console.error('TEST REPORT: Root path not detected at ', rootPath)
}
}