Created
June 6, 2019 12:40
-
-
Save helen-dikareva/70435c233ea57e9567059eab61013fe1 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import http from 'http'; | |
const lazyImport = require('import-lazy')(require); | |
const browserTools = lazyImport('testcafe-browser-tools'); | |
const endpointUtils = lazyImport('endpoint-utils'); | |
const GUID_TITLE = '7cae8e73-44eb-49d8-a275-62a333e6172f'; | |
const WATCH_BROWSER_PAGE = ` | |
<html> | |
<body> | |
<script> | |
document.title="${GUID_TITLE}"; | |
var xhr = new XMLHttpRequest(); | |
xhr.open('GET', './readyToWatch', true); | |
xhr.send(); | |
xhr.onreadystatechange = function() { | |
if (xhr.readyState != 4 || !xhr.responseText) | |
return; | |
window.location = xhr.responseText; | |
}; | |
</script> | |
</body> | |
</html> | |
`; | |
let resolveBrowserClosed = null; | |
let url = ''; | |
let port = 0; | |
let currentBrowserId = null; | |
const createServer = freePort => { | |
port = freePort; | |
http.createServer(async (req, res) => { | |
const isReadyToWatch = req.url.includes('/readyToWatch'); | |
if (isReadyToWatch) { | |
const watchedBrowserId = await browserTools.findWindow(GUID_TITLE); | |
currentBrowserId = watchedBrowserId; | |
browserTools.watchWindow(currentBrowserId).then(() => { | |
if (currentBrowserId === watchedBrowserId) { | |
currentBrowserId = null; | |
resolveBrowserClosed(); | |
} | |
}); | |
} | |
res.writeHead(200, { | |
'Content-Type': isReadyToWatch ? 'text/plain' : 'text/html', | |
'cache-control': 'no-cache, no-store, must-revalidate', | |
'pragma': 'no-cache' | |
}); | |
res.write(isReadyToWatch ? url : WATCH_BROWSER_PAGE); | |
res.end(); | |
}).listen(port); | |
}; | |
export function run (startUrl, browser) { | |
const initPromise = !port ? endpointUtils.getFreePort().then(createServer) : Promise.resolve(); | |
return new Promise((resolver, reject) => { | |
url = startUrl; | |
initPromise | |
.then(() => browserTools.getBrowserInfo(browser)) | |
.then(browserInfo => browserTools.open(browserInfo, `http://localhost:${port}/`)) | |
.catch(reject); | |
resolveBrowserClosed = resolver; | |
}); | |
} | |
export async function close () { | |
if (currentBrowserId) { | |
const id = currentBrowserId; | |
currentBrowserId = null; | |
await browserTools.close(id); | |
} | |
} | |
export async function bringWindowToFront (windowId) { | |
await browserTools.bringWindowToFront(windowId); | |
} | |
export async function bringBrowserToFront () { | |
await bringWindowToFront(currentBrowserId); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { cloneDeep } from 'lodash'; | |
import fs from 'fs'; | |
import Promise from 'promise'; | |
import { Writable } from 'stream'; | |
import createTestCafe, { embeddingUtils } from 'testcafe'; | |
import { join as pathJoin } from 'path'; | |
import RecordRun from './record-run'; | |
import { EventEmitter } from 'events'; | |
import { BrowserWindow, app } from 'electron'; | |
import { run as runBrowser, close as closeBrowser, bringBrowserToFront, bringWindowToFront } from './browser'; | |
import { WIN as IS_WIN, WIN10 as IS_WIN10 } from '../fs/platform'; | |
import { | |
PLAYBACK_STARTED, | |
PLAYBACK_COMMAND, | |
PLAYBACK_FINISHED, | |
COMMANDS_RECORDED, | |
STOP_RECORDING, | |
ELEMENT_PICKED, | |
ELEMENT_PICKING_STARTED, | |
ELEMENT_PICKING_STOPPED, | |
SET_ACTIVE_IFRAME_ERR, | |
RECORDER_ERROR | |
} from './events'; | |
import decorateError from '../testcafe-errors-decoration/decorate-run-time-error'; | |
const RECORDER_SCRIPT = fs.readFileSync(pathJoin(__dirname, './../client/recorder.js')).toString(); | |
const UI_STYLE = fs.readFileSync(pathJoin(__dirname, './../client/css/styles.css')).toString(); | |
const UI_SPRITE = fs.readFileSync(pathJoin(__dirname, './../client/css/sprite.png')); | |
const TESTCAFE_CONFIG_OPTIONS = ['hostname', 'port1', 'port2']; | |
const RECORDING_REPORTER_STREAM = new Writable({ write: () => void 0 }); | |
export default class Recorder extends EventEmitter { | |
constructor (config) { | |
super(); | |
this.testcafe = null; | |
this.testcafeConfig = null; | |
this.recordRun = null; | |
this.fixturePath = ''; | |
this.testName = ''; | |
this.config = config; | |
RecordRun.prototype.recorder = this; | |
this.recordRunPromise = new Promise(resolve => { | |
this.recordRunResolver = resolve; | |
}); | |
} | |
async _waitForRecordRunInit () { | |
//NOTE: RecordRun instance is created once when recorder loads in browser | |
if (this.recordRunPromise) { | |
await this.recordRunPromise; | |
this.recordRunPromise = null; | |
} | |
} | |
_needConfigChange (newTestCafeConfig) { | |
return TESTCAFE_CONFIG_OPTIONS.some( | |
optionName => this.testcafeConfig[optionName] !== newTestCafeConfig[optionName] | |
); | |
} | |
async _handleRuntimeError (error) { | |
this.emit(RECORDER_ERROR, decorateError(error, error.toString())); | |
await this.stop(); | |
} | |
async _getTestCafe (testcafeConfig) { | |
if (!this.testcafe || this._needConfigChange(testcafeConfig)) { | |
if (this.testcafe) | |
await this.testcafe.close(); | |
const { hostname, port1, port2 } = testcafeConfig; | |
this.testcafe = await createTestCafe(hostname, port1, port2); | |
this.testcafeConfig = testcafeConfig; | |
} | |
return this.testcafe; | |
} | |
_onPlaybackCommand (callsite) { | |
this.emit(PLAYBACK_COMMAND, callsite); | |
} | |
_onPlaybackFinished (errs) { | |
this.recordRunResolver(); | |
this.emit(PLAYBACK_FINISHED, errs, () => { | |
this.recordRun.startRecording(); | |
}); | |
} | |
_onElementPickingCanceled () { | |
this.emit(ELEMENT_PICKING_STOPPED); | |
} | |
_onCommandsRecorded (commands) { | |
this.emit(COMMANDS_RECORDED, commands); | |
} | |
_onElementPicked (pickedElementInfo) { | |
const window = BrowserWindow.getAllWindows()[0]; | |
if (IS_WIN) { | |
if (IS_WIN10) { | |
//NOTE: https://github.com/electron/electron/issues/2867#issuecomment-409858459 | |
window.setAlwaysOnTop(true); | |
window.show(); | |
window.setAlwaysOnTop(false); | |
app.focus(); | |
} | |
else | |
bringWindowToFront(window.getTitle()); | |
} | |
else | |
window.focus(); | |
this.emit(ELEMENT_PICKED, pickedElementInfo); | |
} | |
_onSetActiveIframeError (err) { | |
this.emit(SET_ACTIVE_IFRAME_ERR, err); | |
} | |
_onElementPickingStarted (callsite, fieldName) { | |
this.emit(ELEMENT_PICKING_STARTED, callsite, fieldName); | |
bringBrowserToFront(); | |
} | |
async setActiveIframe (commandsData) { | |
await this._waitForRecordRunInit(); | |
await this.recordRun.setActiveIframe(commandsData); | |
} | |
async startElementPicking (callsite, fieldName, iframesOnlyMode) { | |
await this._waitForRecordRunInit(); | |
this.recordRun.startElementPicking(iframesOnlyMode); | |
this._onElementPickingStarted(callsite, fieldName); | |
} | |
async stopElementPicking () { | |
await this._waitForRecordRunInit(); | |
this.recordRun.stopElementPicking(); | |
this._onElementPickingCanceled(); | |
} | |
async executeCommand (command) { | |
const clone = cloneDeep(command); | |
clone.options = { ...clone.options, timeout: 0 }; | |
await this._waitForRecordRunInit(); | |
try { | |
const testCafeCommand = embeddingUtils.createCommandFromObject(clone, this.recordRun); | |
return await this.recordRun.executeAfterPlaybackCommand(testCafeCommand, command.callsite); | |
} | |
catch (err) { | |
throw new Error(this.recordRun.getTestErrorMessage(err)); | |
} | |
} | |
async evaluate (code) { | |
await this._waitForRecordRunInit(); | |
const result = this.recordRun.executeAfterPlaybackCommand({ | |
expression: code, | |
type: embeddingUtils.COMMAND_TYPE.executeExpression | |
}); | |
return result && result.then ? result : Promise.resolve(result); | |
} | |
async highlightElements (selector) { | |
await this._waitForRecordRunInit(); | |
this.recordRun.highlightElements({ type: 'js-expr', value: selector }); | |
} | |
async stopElementsHighlight () { | |
await this._waitForRecordRunInit(); | |
this.recordRun.stopElementsHighlight(); | |
} | |
ensureUploadDirectory (uploadDirPath) { | |
return embeddingUtils.ensureUploadDirectory(uploadDirPath); | |
} | |
copyFilesToUploadFolder (uploadDirPath, files) { | |
return embeddingUtils.copyFilesToUploadFolder(uploadDirPath, files); | |
} | |
async stop () { | |
await closeBrowser(); | |
this.emit(STOP_RECORDING); | |
} | |
async record (recorderOpts, testCafeConfig) { | |
let testCafeRunner = null; | |
let connection = null; | |
try { | |
const testcafe = await this._getTestCafe(testCafeConfig); | |
connection = await testcafe.createBrowserConnection(); | |
testCafeRunner = testcafe.createRunner() | |
.embeddingOptions({ | |
TestRunCtor: RecordRun, | |
assets: [ | |
{ | |
path: '/recorder.js', | |
info: { content: RECORDER_SCRIPT, contentType: 'application/x-javascript' } | |
}, | |
{ | |
path: '/testcafe-recorder-sprite.png', | |
info: { content: UI_SPRITE, contentType: 'image/png' } | |
}, | |
{ | |
path: '/testcafe-recorder-styles.css', | |
info: { content: UI_STYLE, contentType: 'text/css', isShadowUIStylesheet: true } | |
} | |
] | |
}); | |
} | |
catch (e) { | |
await this._handleRuntimeError(e); | |
return; | |
} | |
const { runOptions, src } = recorderOpts; | |
const fixturePath = src.fixturePath; | |
const testName = src.testNames[0].name; | |
this.fixturePath = fixturePath; | |
this.testName = testName; | |
connection.once('ready', () => { | |
this.emit(PLAYBACK_STARTED, fixturePath, testName); | |
testCafeRunner | |
.src(fixturePath) | |
.filter(test => testName === test) | |
.browsers(connection) | |
.reporter('json', RECORDING_REPORTER_STREAM) | |
.useProxy(testCafeConfig.proxy, testCafeConfig.proxyBypass) | |
.run(runOptions) | |
.catch(async error => { | |
await this._handleRuntimeError(error); | |
}); | |
}); | |
connection.once('disconnected', () => { | |
connection.suppressError(); | |
}); | |
runBrowser(connection.url, recorderOpts.browsers) | |
.then(() => this.stop()) | |
.catch(async error => { | |
await this._handleRuntimeError(error); | |
}); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment