Skip to content

Instantly share code, notes, and snippets.

@helen-dikareva
Created June 6, 2019 12:40
Show Gist options
  • Save helen-dikareva/70435c233ea57e9567059eab61013fe1 to your computer and use it in GitHub Desktop.
Save helen-dikareva/70435c233ea57e9567059eab61013fe1 to your computer and use it in GitHub Desktop.
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);
}
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