temp-taiko
Version:
An easy to use wrapper over Chrome Remote Interface.
401 lines (378 loc) • 10.5 kB
JavaScript
const fs = require('fs-extra');
const path = require('path');
const util = require('util');
const { aEval } = require('./awaitEval');
const { initSearch } = require('./repl-search');
const { removeQuotes, symbols } = require('../util');
const { EOL } = require('os');
const funcs = {};
let commands = [];
const stringColor = util.inspect.styles.string;
let taikoCommands = [];
let lastStack = '';
let version = '';
let browserVersion = '';
let doc = '';
module.exports.initialize = async (
taiko,
previousSessionFile,
recordedSession,
) => {
await setVersionInfo();
const repl = require('repl').start({
prompt: '> ',
ignoreUndefined: true,
});
repl.writer = writer(repl.writer);
if (!recordedSession) {
var eventEmitter = taiko.emitter;
eventEmitter.on('success', desc => {
repl.setPrompt('');
process.nextTick(() => {
desc = symbols.pass + desc;
desc = removeQuotes(
util.inspect(desc, { colors: true }),
desc,
);
console.log(desc);
repl.setPrompt('> ');
repl.prompt();
});
});
}
aEval(
repl,
(cmd, res) => !util.isError(res) && commands.push(cmd.trim()),
);
initTaiko(taiko, repl);
initCommands(taiko, repl, previousSessionFile);
initSearch(repl);
return repl;
};
async function setVersionInfo() {
try {
version = require('../../package.json').version;
doc = require('../api.json');
browserVersion = require('../../package.json').taiko
.chromium_version;
} catch (_) {}
displayTaiko();
}
const writer = w => output => {
if (util.isError(output)) return output.message;
else return ' value: ' + w(output);
};
function initCommands(taiko, repl, previousSessionFile) {
repl.defineCommand('trace', {
help: 'Show last error stack trace',
action() {
console.log(
lastStack
? lastStack
: util.inspect(undefined, { colors: true }),
);
this.displayPrompt();
},
});
repl.defineCommand('code', {
help:
'Prints or saves the code for all evaluated commands in this REPL session',
action(file) {
if (!file) console.log(code());
else writeCode(file, previousSessionFile);
this.displayPrompt();
},
});
repl.defineCommand('step', {
help:
'Generate gauge steps from recorded script. (openBrowser and closeBrowser are not recorded as part of step)',
action(file) {
if (!file) console.log(step());
else writeStep(file);
this.displayPrompt();
},
});
repl.defineCommand('version', {
help: 'Prints version info',
action() {
console.log(`${version} (${browserVersion})`);
this.displayPrompt();
},
});
repl.defineCommand('api', {
help: 'Prints api info',
action(name) {
if (!doc) console.log('API usage not available.');
else if (name) displayUsageFor(name);
else displayUsage(taiko);
this.displayPrompt();
},
});
repl.on('reset', () => {
commands.length = 0;
taikoCommands = [];
lastStack = '';
});
repl.on('exit', async () => {
if (taiko.client()) {
await taiko.closeBrowser();
await process.exit();
}
});
}
function code() {
if (commands[commands.length - 1].includes('closeBrowser()'))
commands.pop();
const text = commands
.map(e => {
if (!e.endsWith(';')) e += ';';
return isTaikoFunc(e) ? ' await ' + e : '\t' + e;
})
.join('\n');
const cmds = taikoCommands;
if (!cmds.includes('closeBrowser')) cmds.push('closeBrowser');
const importTaiko =
cmds.length > 0
? `const { ${cmds.join(', ')} } = require('taiko');\n`
: '';
return (
importTaiko +
`(async () => {
try {
${text}
} catch (error) {
console.error(error);
} finally {
await closeBrowser();
}
})();
`
);
}
function step(withImports = false, actions = commands) {
if (actions[0].includes('openBrowser(')) actions = actions.slice(1);
if (
actions.length &&
actions[actions.length - 1].includes('closeBrowser()')
)
actions = actions.slice(0, -1);
const actionsString = actions
.map(e => {
if (!e.endsWith(';')) e += ';';
return isTaikoFunc(e) ? '\tawait ' + e : '\t' + e;
})
.join('\n');
const cmds = taikoCommands.filter(c => {
return c !== 'openBrowser' && c !== 'closeBrowser';
});
const importTaiko =
cmds.length > 0
? `const { ${cmds.join(', ')} } = require('taiko');\n`
: '';
const step = !actionsString
? ''
: `\n// Insert step text below as first parameter\nstep("", async function() {\n${actionsString}\n});\n`;
return !withImports ? step : `${importTaiko}${step}`;
}
function writeStep(file) {
if (fs.existsSync(file)) {
fs.appendFileSync(file, step());
} else {
fs.ensureFileSync(file);
fs.writeFileSync(file, step(true));
}
}
function writeCode(file, previousSessionFile) {
try {
if (fs.existsSync(file)) {
fs.appendFileSync(file, code());
} else {
fs.ensureFileSync(file);
fs.writeFileSync(file, code());
}
if (previousSessionFile) {
console.log(`Recorded session to ${file}.`);
if (path.resolve(file) === path.resolve(previousSessionFile)) {
console.log(
`Please update contents of ${previousSessionFile} before running it with taiko.`,
);
} else {
console.log(
`The previous session was recorded in ${previousSessionFile}.`,
);
console.log(
`Please merge contents of ${previousSessionFile} and ${file} before running it with taiko.`,
);
}
}
} catch (error) {
console.log(`Failed to write to ${file}.`);
console.log(error.stacktrace);
}
}
function initTaiko(taiko, repl) {
const openBrowser = taiko.openBrowser;
taiko.openBrowser = async (options = {}) => {
if (!options.headless) options.headless = false;
return await openBrowser(options);
};
addFunctionToRepl(taiko, repl);
}
function addFunctionToRepl(target, repl) {
for (let func in target) {
if (target[func].constructor.name === 'AsyncFunction') {
repl.context[func] = async function() {
try {
lastStack = '';
let args = await Promise.all(Object.values(arguments));
const res = await target[func].apply(this, args);
if (!taikoCommands.includes(func)) taikoCommands.push(func);
return res;
} catch (e) {
return handleError(e);
} finally {
util.inspect.styles.string = stringColor;
}
};
} else if (target[func].constructor.name === 'Function') {
repl.context[func] = function() {
if (!taikoCommands.includes(func)) taikoCommands.push(func);
const res = target[func].apply(this, arguments);
return res;
};
} else if (
Object.prototype.hasOwnProperty.call(target[func], 'init')
) {
repl.context[func] = target[func];
if (!taikoCommands.includes(func)) taikoCommands.push(func);
}
funcs[func] = true;
}
}
function displayTaiko() {
console.log(`\nVersion: ${version} (Chromium:${browserVersion})`);
console.log('Type .api for help and .exit to quit\n');
}
function displayUsageFor(name) {
const e = doc.find(e => e.name === name);
if (!e) {
console.log(`Function ${name} doesn't exist.${EOL}`);
return;
}
if (e.deprecated) {
console.log(`${EOL}DEPRECATED ${desc(e.deprecated)}${EOL}`);
}
console.log(`${desc(e.description)}${EOL}`);
if (e.params.length > 0) {
console.log(e.params.length > 1 ? 'Parameters:' : 'Parameter:');
console.log(`${EOL}${params(e.params)}${EOL}`);
}
if (e.returns) {
e.returns.map(e => {
console.log(
`Returns: ${type(e.type)} ${
e.description.type ? desc(e.description) : e.description
}${EOL}`,
);
});
}
if (e.examples.length > 0) {
console.log(e.examples.length > 1 ? 'Examples:' : 'Example:');
console.log(
e.examples
.map(e =>
e.description
.split('\n')
.map(e => '\t' + e)
.join('\n'),
)
.join('\n'),
);
}
}
function displayUsage(taiko) {
taiko.metadata.Helpers = taiko.metadata.Helpers.filter(
item => item !== 'repl',
);
for (let k in taiko.metadata)
console.log(`
${removeQuotes(util.inspect(k, { colors: true }), k)}
${taiko.metadata[k].join(', ')}`);
console.log(`
Run \`.api <name>\` for more info on a specific function. For Example: \`.api click\`.
Complete documentation is available at http://taiko.gauge.org.
`);
}
function handleError(e) {
util.inspect.styles.string = 'red';
lastStack = removeQuotes(
util.inspect(e.stack, { colors: true }),
e.stack,
);
e.message =
symbols.fail +
'Error: ' +
e.message +
', run `.trace` for more info.';
return new Error(
removeQuotes(
util.inspect(e.message, { colors: true }),
e.message,
),
);
}
const desc = d =>
d.children
.map(c =>
(c.children || [])
.map((c1, i) => {
if (c1.type === 'listItem')
return (
(i === 0 ? '\n\n* ' : '\n* ') +
c1.children[0].children.map(c2 => c2.value).join('')
);
return (c1.type === 'link'
? c1.children[0].value
: c1.value || ''
).trim();
})
.join(' '),
)
.join(' ');
const type = t => {
switch (t.type) {
case 'NameExpression':
return t.name;
case 'OptionalType':
return type(t.expression);
case 'RestType':
return '...' + type(t.expression);
case 'TypeApplication':
return `${type(t.expression)}<${t.applications
.map(a => type(a))
.join(',')}>`;
case 'UnionType':
return `${t.elements.map(t => type(t)).join('|')}`;
}
};
const param = p => {
const name = p.name || '';
const t = p.type ? type(p.type) + ' - ' : '';
const d =
(p.description ? desc(p.description) : p.description) || '';
const dft = p.default ? `(optional, default ${p.default})` : '';
return `${name} - ${t}${d} ${dft}${EOL}`;
};
const params = p => {
return p
.map(
p =>
'* ' +
param(p) +
(p.properties
? p.properties.map(p => ` * ${param(p)}`).join('')
: ''),
)
.join('');
};
const isTaikoFunc = keyword => keyword.split('(')[0] in funcs;