Created
November 30, 2016 23:25
-
-
Save mgechev/1d35715130a456529baf3ffec16d2d4c to your computer and use it in GitHub Desktop.
This file contains 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
/* 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