Skip to content

Instantly share code, notes, and snippets.

@mgechev
Created November 30, 2016 23:25
Show Gist options
  • Save mgechev/1d35715130a456529baf3ffec16d2d4c to your computer and use it in GitHub Desktop.
Save mgechev/1d35715130a456529baf3ffec16d2d4c to your computer and use it in GitHub Desktop.
/* tslint:disable */
const program =
`
module "login"
go to "app/login"
fill "[email protected]" in "#username"
fill "foobar" in "#password"
click "#login"
test case "login"
test "should login with proper username"
use "login"
wait 5
assert text "#title" "Welcome"
test "should not login without proper username"
go to "app/login"
fill "[email protected]" in "#username"
fill "foobar" in "#password"
click "#login"
wait 2
assert text "#title" "Sorry"
test case "play game"
test "should successfully playgame"
use "login"
go to "app/earn"
click "#whatever"
click "#a1"
code \`if (...) {\`
click "#a2"
code \`} else {\`
click "#a3"
code \`}\`
`
enum TokenType {
Number,
ReservedWord,
String
}
interface CodePosition {
line: number;
character: number;
}
interface Token {
type: TokenType;
lexeme: string | number;
position?: CodePosition;
}
class Lexer {
private current = 0;
constructor(private program: string) {}
lex() {
let result = [];
let current: string;
let line = 0;
let character = 0;
while ((current = this.next())) {
let token: Token;
if (/\n/.test(current)) {
line += 1;
character = 0;
} else {
character += 1;
}
if (!/\s/.test(current)) {
if (current === '"' || current === '`') {
token = this.readString();
} else if (!isNaN(parseInt(current))) {
token = this.readNumber();
} else {
token = this.readReservedWord();
}
token.position = {
line, character
};
result.push(token);
}
}
return result;
}
readString() {
let result = '';
let current: string;
while ((current = this.next()) !== '"' && current !== '`' && !this.end()) {
result += current;
}
return { lexeme: result, type: TokenType.String };
}
readNumber() {
let result = this.program[this.current - 1];
let current: string;
while (!isNaN(parseInt(current = this.next())) && !this.end()) {
result += current;
}
return { lexeme: parseInt(result), type: TokenType.Number };
}
readReservedWord() {
let result = this.program[this.current - 1];
let current: string;
while ((current = this.next()) !== ' ' && current && !this.end()) {
result += current;
}
return { lexeme: result, type: TokenType.ReservedWord };
}
next() {
return this.program[this.current++];
}
end() {
return this.current >= this.program.length;
}
}
const lexer = new Lexer(program);
const tokens = lexer.lex();
class ModuleAst {
name: string;
operations: OperationAst[] = [];
}
class GotoAst {
url: string;
}
class FillAst {
text: string;
where: string;
}
class ClickAst {
where: string;
}
class AssertTextAst {
text: string;
selector: string;
}
class WaitAst {
duration: number;
}
class CustomCodeAst {
code: string;
}
type Operand = string | number;
class OperationAst {
name: string;
operands: Operand[] = [];
}
class TestAst {
name: string;
operations: OperationAst[] = [];
}
class TestCaseAst {
name: string;
tests: TestAst[] = [];
}
class UseAst {
name: string;
}
class ProgramAst {
modules: ModuleAst[] = [];
testCases: TestCaseAst[] = [];
}
class Parser {
private current = 0;
constructor(private tokens: Token[]) {}
parse() {
return this.parseProgramAst();
}
parseProgramAst() {
const program = new ProgramAst();
while (!this.end()) {
const current = this.next();
if (current.type === TokenType.ReservedWord) {
if (current.lexeme === 'module') {
program.modules.push(this.parseModule());
} else if (current.lexeme === 'test') {
let next = this.next();
if (next && next.type === TokenType.ReservedWord && next.lexeme === 'case') {
// Skipping the token "case"
program.testCases.push(this.parseTestCase());
} else {
this.report(current, 'Unexpected token "test". Test cases should be on top level.');
}
} else {
this.report(current, 'Unexpected token. Only modules and test cases are allowed on top level.');
}
} else {
this.report(current, 'Unexpected token. Only reserved words are allowed on top level.');
}
}
program.testCases.forEach(t => {
t.tests = t.tests.filter(t => t.operations.length);
});
program.modules = program.modules.filter(m => m.operations.length);
return program;
}
parseModule() {
let current, next = this.next();
const module = new ModuleAst();
if (next.type === TokenType.String) {
module.name = next.lexeme as string;
}
while (!this.end()) {
current = next;
next = this.next();
this.current -= 1;
if (this.isTestCaseOrTestOrModule(current, next)) {
return module
}
const operator = this.readOperation();
if (operator) {
module.operations.push(operator);
} else {
this.current -= 1;
}
}
return module;
}
parseTestCase() {
let current = this.next();
const testCase = new TestCaseAst();
testCase.name = current.lexeme as string;
let next = this.next();
current = next;
while (!this.end()) {
current = next;
next = this.next();
this.current -= 1;
if (this.isTestCaseOrModule(current, next)) {
this.current -= 1;
return testCase;
}
testCase.tests.push(this.parseTest());
}
return testCase;
}
parseTest() {
let current, next = this.next();
let name = next.lexeme as string;
const test = new TestAst();
test.name = name;
while (!this.end()) {
current = next;
next = this.next();
this.current -= 1;
if (this.isTestCaseOrTestOrModule(current, next)) {
return test
}
const operator = this.readOperation();
if (operator) {
test.operations.push(operator);
} else {
this.current -= 1;
}
}
return test;
}
readOperation() {
let current = this.next();
let next = this.next();
let operator: any;
if (!current || !next || this.isTestCaseOrTestOrModule(current, next)) {
this.current -= 1;
return operator;
}
if (current.lexeme === 'go' && current.type === TokenType.ReservedWord) {
if (next && next.lexeme === 'to' && next.type === TokenType.ReservedWord) {
next = this.next();
if (!next || next.type !== TokenType.String) {
this.report(current, 'go to should receive a URL of type string');
}
const ast = new GotoAst();
ast.url = <string>next.lexeme;
return ast;
} else {
this.report(current, '"go" must be followed by "to"');
}
}
if (current.lexeme === 'assert' && current.type === TokenType.ReservedWord) {
if (next && next.lexeme === 'text' && next.type === TokenType.ReservedWord) {
next = this.next();
if (!next || next.type !== TokenType.String) {
this.report(current, 'assert text should a selector and a text');
} else {
const selector = next.lexeme;
next = this.next();
if (next && next.type === TokenType.String) {
const ast = new AssertTextAst();
ast.selector = selector as string;
ast.text = next.lexeme as string;
return ast;
} else {
this.report(current, 'assert text should a selector and a text');
}
}
} else {
this.report(current, '"go" must be followed by "to"');
}
}
if (current.lexeme === 'fill' && current.type === TokenType.ReservedWord) {
if (next && next.type === TokenType.String) {
const text = next.lexeme;
next = this.next();
if (next.lexeme === 'in' && next.type === TokenType.ReservedWord) {
next = this.next();
if (next.type === TokenType.String) {
const ast = new FillAst();
ast.text = text as string;
ast.where = next.lexeme as string;
return ast;
} else {
this.report(current, 'fill "text" in "elementSelector"');
}
} else {
this.report(current, 'fill "text" in "elementSelector"');
}
} else {
this.report(current, 'fill "text" in "elementSelector"');
}
}
if (current.lexeme === 'click' && current.type === TokenType.ReservedWord) {
if (next && next.type === TokenType.String) {
const ast = new ClickAst();
ast.where = next.lexeme as string;
return ast;
} else {
this.report(current, 'click "cssSelector"');
}
}
if (current.lexeme === 'code' && current.type === TokenType.ReservedWord) {
if (next && next.type === TokenType.String) {
const ast = new CustomCodeAst();
ast.code = next.lexeme as string;
return ast;
} else {
this.report(current, 'code `cssSelector`');
}
}
if (current.lexeme === 'wait' && current.type === TokenType.ReservedWord) {
if (next && next.type === TokenType.Number) {
const ast = new WaitAst();
ast.duration = next.lexeme as number;
return ast;
} else {
this.report(current, 'wait duration');
}
}
if (current.lexeme === 'use' && current.type === TokenType.ReservedWord) {
if (next && next.type === TokenType.String) {
const ast = new UseAst();
ast.name = next.lexeme as string;
return ast;
} else {
this.report(current, 'use "module-name"');
}
}
this.report(current, 'unknown operator');
}
isTestCaseOrTestOrModule(token, next) {
if (this.isTestCaseOrModule(token, next) ||
(token.type === TokenType.ReservedWord && token.lexeme === 'test')) {
return true
}
return false;
}
isTestCaseOrModule(token, next) {
if (token.type === TokenType.ReservedWord && token.lexeme === 'module') {
return true;
} else if (token.type === TokenType.ReservedWord && token.lexeme === 'test' &&
next.type === TokenType.ReservedWord && next.lexeme === 'case') {
return true;
}
return false;
}
report(token, message) {
throw new Error(message + `(${token.position.line}, ${token.position.character})`);
}
next() {
return this.tokens[this.current++];
}
end() {
return this.current >= this.tokens.length;
}
}
const parser = new Parser(tokens);
const ast = parser.parse();
interface SymbolTable {
[key: string]: ModuleAst;
}
class OperationVisitor {
visit(operation: any, symbolTable: SymbolTable, prefix: string) {
if (operation instanceof GotoAst) {
return `${prefix}browser.get('${operation.url}');\n`;
} else if (operation instanceof WaitAst) {
return `${prefix}browser.wait(new Promise(r => setTimeout(r, ${operation.duration})));\n`;
} else if (operation instanceof FillAst) {
return `${prefix}element(by.css('${operation.where}')).sendKeys('${operation.text}');\n`;
} else if (operation instanceof CustomCodeAst) {
return prefix + operation.code + '\n';
} else if (operation instanceof AssertTextAst) {
return `${prefix}expect(element(by.css('${operation.selector}')).getText()).toEqual('${operation.text}');\n`;
} else if (operation instanceof ClickAst) {
return `${prefix}element(by.css('${operation.where}')).click();\n`;
} else if (operation instanceof UseAst) {
return new ExpressionListVisitor().visit(symbolTable[operation.name].operations, symbolTable, prefix);
}
}
}
class ExpressionListVisitor {
visit(operations: OperationAst[], symbolTable: SymbolTable, prefix: string) {
return operations.map(o => {
return new OperationVisitor().visit(o, symbolTable, prefix);
}).join('');
}
}
class TestVisitor {
visit(test: TestAst, symbolTable: SymbolTable, prefix: string) {
let result = `${prefix}it('${test.name}', () => {\n`;
result += new ExpressionListVisitor().visit(test.operations, symbolTable, prefix + ' ');
result += `${prefix}});\n`;
return result;
}
}
class TestCaseVisitor {
visit(testCase: TestCaseAst, symbolTable: SymbolTable) {
let result = `describe('${testCase.name}', () => {\n`;
result += testCase.tests.map(t => new TestVisitor().visit(t, symbolTable, ' ')).join('\n');
result += `});\n\n`;
return result;
}
}
class ProgramVisitor {
visit(program: ProgramAst) {
const symbolTable = {};
program.modules.forEach(m => symbolTable[m.name] = m);
return program.testCases.map(t => new TestCaseVisitor().visit(t, symbolTable)).join('');
}
}
console.log(new ProgramVisitor().visit(ast));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment