const puppy = window.puppy
const pup = do(ns,...params)
if ns.match(/^spec/) and puppy
puppy(ns,params)
return
# return
if puppy
await puppy(ns,params)
class PupKeyboard
def type text, options = {}
await puppy('keyboard.type',[text,options])
def down text, options = {}
await puppy('keyboard.down',[text,options])
def up text, options = {}
await puppy('keyboard.up',[text,options])
def press text, options = {}
await puppy('keyboard.press',[text,options])
class PupMouse
def type text, options
puppy('keyboard.type',[text,options])
const TERMINAL_COLOR_CODES =
bold: 1
underline: 4
reverse: 7
black: 30
red: 31
green: 32
yellow: 33
blue: 34
magenta: 35
cyan: 36
white: 37
const fmt = do(code,string)
return string.toString! if console.group
code = TERMINAL_COLOR_CODES[code]
let resetStr = "\x1B[0m"
let resetRegex = /\x1B\[0m/g
let codeRegex = /\x1B\[\d+m/g
let tagRegex = /(<\w+>|)|(<\/\w+>|)/i
let numRegex = /\d+/
let str = ('' + string).replace(resetRegex, "{resetStr}\x1B[{code}m") # allow nesting
str = "\x1B[{code}m{str}{resetStr}"
return str
class SpecComponent
def log ...params
root.console.log(*params)
def emit ev, pars
imba.emit(self,ev,pars)
get root
parent ? parent.root : self
global class Spec < SpecComponent
get keyboard
_keyboard ||= new PupKeyboard
get mouse
_mouse ||= new PupMouse
def click sel, trusted = yes
if puppy and trusted
console.log "click with puppeteer!!",sel
try
await puppy('click',[sel])
catch e
console.log 'error from pup click!'
else
let el = document.querySelector(sel)
el && el.click!
await tick!
def tick commit = true
imba.commit! if commit
await imba.scheduler.promise
observer.takeRecords!
def wait time = 100
new Promise(do(resolve) setTimeout(resolve,time))
def constructor
super()
console = console
blocks = []
assertions = []
stack = [context = self]
tests = []
warnings = []
state = {info: [], mutations: [], log: [], commits: 0}
observer = new MutationObserver do(muts)
context.state.mutations.push(...muts)
self
get fullName
""
def eval block, ctx
stack.push(context = ctx)
let res = block(context.state)
let after = do
stack.pop!
context = stack[stack.length - 1]
observer.takeRecords!
self
let err = do(e)
ctx.error = e
after!
if res and res.then
return res.then(after,err)
else
after!
return Promise.resolve(self)
def describe name, blk
blocks.push new SpecGroup(name, blk, self)
def test name, blk
if name isa Function
blk = name
name = context.blocks.length + 1
context.blocks.push new SpecExample(name, blk, context)
def eq actual, expected, options
new SpecAssert(context, actual,expected, options)
def step i = 0, &blk
Spec.CURRENT = self
let block = blocks[i]
return self.finish! unless block
imba.once(block,'done') do self.step(i+1)
block.run!
def run
new Promise do(resolve,reject)
pup("spec:start",{})
let prevInfo = console.info
let fn = do context.state.commits++
imba.scheduler.on('commit',fn)
observer.observe(document.body,{
attributes: true,
childList: true,
characterData: true,
subtree: true
})
console.log 'running spec'
console.info = do(...params)
context.state.info.push(params)
context.state.log.push(params[0])
imba.once(self,'done') do
observer.disconnect!
console.info = prevInfo
imba.scheduler.un('commit',fn)
resolve!
await tick!
self.step(0)
def finish
let ok = []
let failed = []
for test in tests
test.failed ? failed.push(test) : ok.push(test)
let logs = [
fmt('green',"{ok.length} OK")
fmt('red',"{failed.length} FAILED")
"{tests.length} TOTAL"
]
console.log logs.join(" | ")
let exitCode = (failed.length == 0 ? 0 : 1)
emit('done', [exitCode])
pup("spec:done",{
failed: failed.length,
passed: ok.length,
warnings: warnings.length
})
global class SpecGroup < SpecComponent
def constructor name, blk, parent
super()
parent = parent
name = name
blocks = []
SPEC.eval(blk,self) if blk
self
get fullName
"{parent.fullName}{name} > "
def describe name, blk
blocks.push new SpecGroup(name, blk, self)
def test name, blk
blocks.push new SpecExample(name, blk, self)
def run i = 0
start! if i == 0
let block = blocks[i]
return finish! unless block
imba.once(block,'done') do run(i+1)
block.run! # this is where we wan to await?
def start
emit('start', [self])
if console.group
console.group(name)
else
console.log "\n-------- {name} --------"
def finish
console.groupEnd(name) if console.groupEnd
emit('done', [self])
global class SpecExample < SpecComponent
def constructor name, block, parent
super()
parent = parent
evaluated = no
name = name
block = block
assertions = []
root.tests.push(self)
state = {info: [], mutations: [], log: []}
self
get fullName
"{parent.fullName}{name}"
def run
start!
# does a block really need to run here?
try
let promise = (block ? SPEC.eval(block, self) : Promise.resolve({}))
let res = await promise
catch e
console.log "error from run!",e
error = e
evaluated = yes
finish!
def start
emit('start')
console.group(fullName)
def finish
failed ? fail! : pass!
let fails = assertions.filter do $1.critical
pup("spec:test", name: fullName, failed: failed, messages: fails.map(do $1.toString!), error: error && error.message )
for ass in assertions
if ass.failed
if ass.options.warn
pup("spec:warn",message: ass.toString!)
else
pup("spec:fail",message: ass.toString!)
console.groupEnd(fullName)
emit('done',[self])
def fail
console.log("%c✘ {fullName}", "color:orangered",state)
def pass
console.log("%c✔ {fullName}", "color:forestgreen")
get failed
error or assertions.some do(ass) ass.critical
get passed
!failed!
global class SpecAssert < SpecComponent
def constructor parent,actual,expected,options = {}
super()
parent = parent
expected = expected
actual = actual
options = options
message = (options.message || options.warn) || "expected %2 - got %1"
parent.assertions.push(self)
self.compare(expected,actual) ? pass! : fail!
self
def compare a,b
if a === b
return true
if a isa Array and b isa Array
return false if a.length != b.length
for item,i in a
return false unless self.compare(item,b[i])
return JSON.stringify(a) == JSON.stringify(b)
return false
get critical
failed && !options.warn
def fail
failed = yes
if options.warn
root.warnings.push(self)
self
def pass
passed = yes
self
def toString
if failed and message isa String
let str = message
str = str.replace('%1',actual)
str = str.replace('%2',expected)
return str
else
"failed"
window.spec = global.SPEC = new Spec
# global def p do console.log(*arguments)
global def describe name, blk do SPEC.context.describe(name,blk)
global def test name, blk do SPEC.test(name,blk)
global def eq actual, expected, o do SPEC.eq(actual, expected, o)
global def ok actual, o do SPEC.eq(!!actual, true, o)
global def eqcss el, match,sel,msg
if typeof el == 'string'
el = document.querySelector(el)
elif el isa Element and !el.parentNode
document.body.appendChild(el)
if typeof sel == 'string'
el = el.querySelector(sel)
elif typeof sel == 'number'
el = el.children[sel]
let style = window.getComputedStyle(el)
if typeof match == 'number'
match = {fontWeight: String(match)}
for own k,expected of match
let real = style[k]
if expected isa RegExp
global.ok real.match(expected)
unless real.match(expected)
console.log real,'did no match',expected
else
global.eq(real,expected)
return
window.onerror = do(e)
console.log('page:error',{message: (e.message or e)})
window.onunhandledrejection = do(e)
console.log('page:error',{message: e.reason.message})