UNPKG

@tremho/jove-test

Version:
229 lines (203 loc) 8.47 kB
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) } }