dce-selenium
Version:
Selenium library to simplify testing and automatically snapshot the DOM.
360 lines (303 loc) • 11.1 kB
JavaScript
const fs = require('fs');
const path = require('path');
// Add drivers to path
require('chromedriver');
require('geckodriver');
const buildDriver = require('./buildDriver');
const surroundWithBuffer = require('./surroundWithBuffer');
const consoleLog = require('./consoleLog');
// Try to import custom commands
const cwd = process.env.PWD;
let customCommands;
try {
/* eslint-disable import/no-dynamic-require */
/* eslint-disable global-require */
customCommands = require(path.join(cwd, 'test', 'selenium', 'commands'));
} catch (err) {
customCommands = {};
}
/*------------------------------------------------------------------------*/
/* Printing Helpers */
/*------------------------------------------------------------------------*/
/* eslint-disable no-console */
const W = process.stdout.columns;
/*------------------------------------------------------------------------*/
/* Build driver */
/*------------------------------------------------------------------------*/
const printWithBorder = (toPrint, error) => {
const str = (
typeof toPrint === 'object'
? JSON.stringify(toPrint)
: String(toPrint)
);
const naturalLines = str.split('\n');
const indentChar = (error ? '' : '> ');
const noIndentChar = (error ? '' : ' ');
const buffer = (error ? 4 : 6);
const startCode = (error ? '\x1b[31m' : '');
const endCode = (error ? '\x1b[0m' : '');
naturalLines.forEach((naturalLine) => {
const lines = [];
let i = 0;
while (i < naturalLine.length) {
lines.push(
naturalLine.substring(i, Math.min(i + W - buffer, naturalLine.length))
);
i += W - buffer;
}
lines.forEach((line, index) => {
const lineToPrint = `\u2551${startCode} ${index === 0 ? indentChar : noIndentChar}${line}${' '.repeat(W - buffer - line.length)} ${endCode}\u2551`;
consoleLog(lineToPrint);
});
});
};
let prevDriver;
const getDriver = async () => {
// Kill previous driver
if (prevDriver) {
await prevDriver.quit();
}
// Build a driver
const driver = buildDriver(printWithBorder);
prevDriver = driver;
// Add in custom commands
Object.keys(customCommands).forEach((commandName) => {
// Don't allow custom commands to overwrite existing ones
if (driver[commandName]) {
console.log(`\nCould not create custom command "${commandName}"!`);
console.log('Reason: function already exists (no overwrite allowed)');
driver.quit();
process.exit(0);
}
// Add custom command
driver[commandName] = customCommands[commandName].bind(driver);
});
await driver.webdriver.get('about:blank');
return driver;
};
/*------------------------------------------------------------------------*/
/* Keep track of describe level */
/*------------------------------------------------------------------------*/
// Name describe calls using letters
const LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
// Keep nested list of describes (so snapshots can mirror describe structure in
// folders)
const describeIndices = [-1];
const describeLevels = [];
global.describeBeforeEachMap = {};
global.describeAfterEachMap = {};
// Create a new describe function that keeps track of describe level
const originalDescribe = describe;
const genDescribe = (name) => {
return async (title, test) => {
// Get describe index (and increment)
const index = describeIndices.pop() + 1;
describeIndices.push(index);
// Create a letter marker based on the describe index
const marker = (
index < LETTERS.length
? LETTERS[index]
: LETTERS[LETTERS.length - 1] + (index - LETTERS.length)
);
// Add new level or titles and indices
const newTitle = `${marker} - ${title}`;
// Call original describe function
/* eslint-disable prefer-arrow-callback */
/* eslint-disable func-names */
const describeToCall = (name ? originalDescribe[name] : originalDescribe);
await describeToCall(newTitle, async function (...args) {
// Add level of title and index
// eslint-disable-next-line no-undef
before(() => {
describeLevels.push(newTitle);
describeIndices.push(-1);
global.describeTitle = newTitle;
});
// Remove level of title and index
// eslint-disable-next-line no-undef
after(() => {
global.describeTitle = null;
describeIndices.pop();
describeLevels.pop();
});
// Create beforeEachS, afterEachS functions
const beforeEachS = (handler) => {
global.describeBeforeEachMap[newTitle] = handler;
};
const afterEachS = (handler) => {
global.describeAfterEachMap[newTitle] = handler;
};
// Add containing tests
return test(beforeEachS, afterEachS, ...args);
});
};
};
global.describeS = genDescribe();
global.describeS.skip = genDescribe('skip');
global.describeS.only = genDescribe('only');
/*------------------------------------------------------------------------*/
/* Create itS */
/*------------------------------------------------------------------------*/
const setup = () => {
// Create itS function for mocha testing
let testIndex = 0;
// Gen itS sub-function
const genIt = (name) => {
return (title, a, b) => {
let test;
let timeout;
if (b) {
test = b;
timeout = a;
} else {
test = a;
timeout = 45000;
}
// Increment test number
testIndex += 1;
const thisTestIndex = testIndex;
// Call normal "it" function
/* eslint-disable func-names */
const newTitle = `${thisTestIndex} - ${title}`;
// eslint-disable-next-line no-undef
(name ? it[name] : it)(newTitle, async function () {
// Add initial timeout that allows enough time to create a window
this.timeout(600000); // 1 minute
// Keep track of setup time so we can add that to timeout
const testStartTime = Date.now();
// Update global names (for snapshot naming)
global.testTitle = newTitle;
global.describeFolder = (
describeLevels.length > 0
? describeLevels.join('/')
: null
);
// Create a new driver
const driver = await getDriver();
// Printing functions
const logStart = (actualTitle = newTitle) => {
consoleLog('\u2554' + '\u2550'.repeat(W - 2) + '\u2557');
consoleLog(surroundWithBuffer(actualTitle, '\u2551'));
};
const logEnd = () => {
consoleLog('\u255A' + '\u2550'.repeat(W - 2) + '\u255D');
};
// Before each
const title = global.describeTitle;
const beforeEachFunction = global.describeBeforeEachMap[title];
if (beforeEachFunction) {
logStart('Before Each');
await beforeEachFunction(driver);
logEnd();
}
// Log start of test
logStart();
// Find setup time
const setupTime = Date.now() - testStartTime;
// Set default timeout to very large
this.timeout(timeout + setupTime);
// Run the test
let error;
try {
// Test
await test(driver);
} catch (err) {
let { message } = err;
const driverDead = (err.name === 'NoSuchSessionError');
// ^ also happens when a timeout occurred
if (driverDead) {
// Timeout occurred
message = `Either test timed out at ${timeout}ms (${timeout + setupTime}ms with setup) or we could not get a valid browser session (another automated window is still open or driver not set up correctly)`;
error = new Error(message);
} else {
error = err;
}
// Print error
consoleLog(surroundWithBuffer('Test Failed! Error message:', '\u2551'));
printWithBorder(message, true);
consoleLog(surroundWithBuffer('See full stacktrace after test results', '\u2551'));
}
// Log end of test
logEnd();
// After each
const afterEachFunction = global.describeAfterEachMap[title];
if (afterEachFunction) {
logStart('After Each');
await afterEachFunction(driver);
logEnd();
}
// Clean up driver session
await driver.quit();
if (error) {
throw error;
}
});
};
};
// Buid itS
global.itS = genIt();
global.itS.only = genIt('only');
global.itS.skip = genIt('skip');
};
/*------------------------------------------------------------------------*/
/* Initialization Function */
/*------------------------------------------------------------------------*/
// Get timestamp of current date
const startTime = new Date();
const replaceAll = (str, search, replacement) => {
return str.replace(new RegExp(search, 'g'), replacement);
};
const timestamp = `${startTime.toLocaleDateString()} ${replaceAll(startTime.toLocaleTimeString(), ':', '-')}`;
const init = (config = {}) => {
// Get browser and snapshot name from arguments
let browser = 'chrome';
let headless = false;
let snapshotName = timestamp;
for (let i = 0; i < process.argv.length; i++) {
const arg = process.argv[i];
if (arg === '--chrome') {
browser = 'chrome';
} else if (arg === '--safari') {
browser = 'safari';
} else if (arg === '--safari-stp') {
browser = 'safari-technology-preview';
} else if (arg === '--firefox') {
browser = 'firefox';
} else if (arg === '--headless-chrome') {
browser = 'chrome';
headless = true;
} else if (arg.startsWith('--snapshot-title ')) {
snapshotName = arg.replace('--snapshot-title ', '');
}
}
// Set globals
global.dceSeleniumConfig = {
headless,
browser,
snapshotName,
browserDescription: (
headless
? `Headless ${browser.charAt(0).toUpperCase()}${browser.substring(1)}`
: `${browser.charAt(0).toUpperCase()}${browser.substring(1)}`
),
defaultHost: config.defaultHost || null,
dontUseHTTPS: config.dontUseHTTPS,
noSnapshots: config.noSnapshots,
};
// Initialize driver and tests
setup();
};
/*------------------------------------------------------------------------*/
/* Initialize by Config File */
/*------------------------------------------------------------------------*/
// Read in config
let config;
try {
const configPath = path.join(cwd, 'test', 'selenium', 'config.json');
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
} catch (err) {
config = {};
}
// Initialize with defaults just in case no call by programmer
init(config);
module.exports = init;