spectacular
Version:
Advanced BDD framework for CoffeeScript and JavaScript
447 lines (349 loc) • 12.2 kB
text/coffeescript
spectacular.helpers = new spectacular.GlobalizableObject
spectacular.helper = (name, value) -> spectacular.helpers.set name, value
class spectacular.Environment
@include spectacular.Globalizable
constructor: (@options) ->
@globalizable = 'it xit describe xdescribe expect
before after given subject its itsInstance itsReturn
withParameters fail pending success skip should shouldnt
dependsOn spyOn whenPass fixture except only sharedExample
itBehavesLike fixturePath registerFixtureHandler'.split(/\s+/g)
@rootExampleGroup = new spectacular.ExampleGroup null, ''
@currentExampleGroup = @rootExampleGroup
@currentExample = null
@runner = new spectacular.Runner(@rootExampleGroup, @options, this)
@sharedExamples = {}
@createExampleAlias 'the'
@createExampleAlias 'specify'
@createExampleGroupAlias 'context'
@createOuterExampleAlias 'xcontext', 'xdescribe'
@createOuterExampleAlias 'withArguments', 'withParameters'
@createOuterExampleAlias 'itShould', 'itBehavesLike'
@registerFixtureHandler 'json', @handleJSONFixture
@registerFixtureHandler 'html', @handleHTMLFixture
@registerFixtureHandler 'dom', @handleDOMFixture
run: => @runner.run()
@_globalize = Environment::globalize
@_unglobalize = Environment::unglobalize
createGlobalizedMethod: (name, block) ->
unless name in @globalizable
@globalizable.push name
@[name] = block
@globalizeMember name, block if @globalized
createExampleGroupAlias: (name) -> @createOuterExampleAlias name, 'describe'
createExampleAlias: (name) -> @createOuterExampleAlias name, 'it'
createInnerExampleAlias: (newName, oldName) ->
@createGlobalizedMethod newName, ->
@notOutsideIt newName
@[oldName].apply this, arguments
createOuterExampleAlias: (newName, oldName) ->
@createGlobalizedMethod newName, ->
@notInsideIt newName
@[oldName].apply this, arguments
globalize: ->
Environment._globalize.call(this)
spectacular.factories.globalize()
spectacular.matchers.globalize()
spectacular.helpers.globalize()
@globalizeObjectExtensions()
unglobalize: ->
Environment._unglobalize.call(this)
spectacular.factories.unglobalize()
spectacular.matchers.unglobalize()
spectacular.helpers.unglobalize()
@unglobalizeObjectExtensions()
unglobalizeObjectExtensions: ->
delete Object::should
delete Object::shouldnt
delete spectacular.global.should
delete spectacular.global.shouldnt
globalizeObjectExtensions: ->
env = this
Object.defineProperty Object.prototype, 'should', {
writable: true
configurable: true
enumerable: false
value: (matcher, neg=false) ->
env.notOutsideIt 'should'
return unless matcher?
env.currentExample.result.addExpectation(
new spectacular.Expectation(
env.currentExample,
@valueOf(),
matcher,
neg,
new Error
)
)
}
Object.defineProperty Object.prototype, 'shouldnt', {
writable: true
configurable: true
enumerable: false
value: (matcher) ->
env.notOutsideIt 'should'
@should matcher, true
}
clone: ->
optionsCopy = {}
optionsCopy[k] = v for k,v of @options
new spectacular.Environment optionsCopy
notInsideIt: (method) ->
if @currentExample?
throw new Error "#{method} called inside a it block"
notOutsideIt: (method) ->
unless @currentExample?
throw new Error "#{method} called outside a it block"
notWihoutMatcher: (method) ->
throw new Error "#{method} called without a matcher"
fail: ->
error = new Error 'Failed'
if error.stack?
stack = error.stack.split('\n')
specIndex = spectacular.env.runner.findSpecFileInStack stack
error.stack = stack[specIndex..].join('\n') if specIndex isnt -1
@currentExample.reject error
pending: -> @currentExample.pending()
skip: -> @currentExample.skip()
success: ->
it: (msgOrBlock, block) ->
@notInsideIt 'it'
[msgOrBlock, block] = ['', msgOrBlock] if typeof msgOrBlock is 'function'
example = new spectacular.Example block, msgOrBlock, @currentExampleGroup
@currentExampleGroup.addChild example
example
xit: (msgOrBlock, block) ->
@notInsideIt 'xit'
if typeof msgOrBlock is 'string'
@it msgOrBlock, -> pending()
else
@it -> pending()
before: (block) ->
@notInsideIt 'before'
@currentExampleGroup.ownBeforeHooks.push block
after: (block) ->
@notInsideIt 'after'
@currentExampleGroup.ownAfterHooks.push block
its: (property, block) ->
@notInsideIt 'its'
parentSubjectBlock = @currentExampleGroup.subjectBlock
@emptyContext =>
@subject property, -> parentSubjectBlock?.call(this)[property]
@specify "the #{property} property", block
itsInstance: (property, options, block) ->
@notInsideIt 'itsInstance'
if typeof property is 'function'
[property, options, block] = [block, {}, property]
else if typeof property is 'object'
[property, options, block] = [null, property, options]
else if typeof options is 'function'
[options, block] = [{}, options]
parentSubjectBlock = @currentExampleGroup.subjectBlock
unless parentSubjectBlock?
throw new Error 'itsInstance called in context without a previous subject'
@emptyContext =>
@subject 'instance', ->
params = if options.with?
if typeof options.with is 'function'
options.with.call(this)
else
options.with
else
@parameters or []
build parentSubjectBlock?.call(this), params
if property?
@its property, block
else
@specify 'the instance', block
itsReturn: (options, block) ->
@notInsideIt 'itsReturn'
[block, options] = [options, {}] if typeof options is 'function'
parentSubjectBlock = @currentExampleGroup.subjectBlock
unless parentSubjectBlock?
throw new Error 'itsReturn called in context without a previous subject'
@emptyContext =>
@subject 'returnedValue', ->
context = if options.inContext? or options.in_context?
ctx = options.inContext or options.in_context
if typeof ctx is 'function'
ctx.call(this)
else
ctx
else
this
params = if options.with?
if typeof options.with is 'function'
options.with.call(this)
else
options.with
else
@parameters or []
parentSubjectBlock?.call(this).apply(context, params)
@specify 'the returned value', block
subject: (name, block) ->
@notInsideIt 'subject'
[name, block] = [block, name] if typeof name is 'function'
subjectBlock = ->
@["__#{name or 'subject'}"] ||= block.call this
@currentExampleGroup.ownSubjectBlock = subjectBlock
@given name, subjectBlock if name?
given: (name, block) ->
@notInsideIt 'given'
@before ->
Object.defineProperty this, name, {
configurable: true
enumerable: true
get: -> @["__#{name}"] ||= block.call(this)
}
describe: (subject, options, block) ->
[options, block] = [block, options] if typeof options is 'function'
@notInsideIt 'describe'
oldGroup = @currentExampleGroup
@currentExampleGroup = currentGroup = new spectacular.ExampleGroup(
block, subject, oldGroup, options
)
oldGroup.addChild @currentExampleGroup
try
@currentExampleGroup.executeBlock()
catch error
finally
@currentExampleGroup = oldGroup
throw error if error?
currentGroup
xdescribe: (subject, options, block) ->
@notInsideIt 'xdescribe'
[options, block] = [block, options] if typeof options is 'function'
describe subject, -> it -> pending()
emptyContext: (block) -> @context '', block
withParameters: (args...) ->
@notInsideIt 'withParameters'
@given 'parameters', ->
if typeof args[0] is 'function'
args[0].call(this)
else
args
dependsOn: (spec) ->
@notInsideIt 'dependsOn'
@currentExampleGroup.ownDependencies.push spec
whenPass: (block) ->
@notInsideIt 'whenPass'
previousContext = @currentExampleGroup
@emptyContext =>
@currentExampleGroup.ownCascading = previousContext
block()
spyOn: (obj, method) ->
@notOutsideIt 'spyOn'
oldMethod = obj[method]
context = @currentExample.context
spy = (args...) ->
spy.argsForCall.push args
if spy.mock?
spy.mock.apply(obj, args)
else
oldMethod.apply(obj, args)
spy.spied = oldMethod
spy.argsForCall = []
spy.andCallFake = (@mock) -> this
spy.andReturns = (value) -> spy.andCallFake -> value
spy.andCallThrough = (block) ->
@mock = ->
block.call context, oldMethod.apply this, arguments
this
utils.snakify spy
@currentExample.ownAfterHooks.push ->
obj[method] = oldMethod
obj[method] = spy
spy
should: (matcher, neg=false) ->
@notOutsideIt 'should'
@notWihoutMatcher 'should' unless matcher?
@currentExample.result.addExpectation(
new spectacular.Expectation(
@currentExample,
@currentExample.subject,
matcher,
neg,
new Error
)
)
shouldnt: (matcher) -> @should matcher, true
expect: (desc, value) ->
value = desc if not value? and arguments.length < 2
@notOutsideIt 'expect'
self = this
err = new Error
to: (matcher) ->
self.notWihoutMatcher 'expect(...).to' unless matcher?
self.currentExample.result.addExpectation(
new spectacular.Expectation(
self.currentExample,
value,
matcher,
false,
err,
desc
)
)
not:
to: (matcher) ->
self.notWihoutMatcher 'expect(...).not.to' unless matcher?
self.currentExample.result.addExpectation(
new spectacular.Expectation(
self.currentExample,
value,
matcher,
true,
err,
desc
)
)
except: (example) -> example.inclusive = true
only: (example) -> example.exclusive = true
sharedExample: (name, block) ->
if name of @sharedExamples
throw new Error "shared example '#{name}' already registered"
@sharedExamples[name] = block
itBehavesLike: (name, options={}) ->
unless name of @sharedExamples
throw new Error "shared example '#{name}' not found"
@sharedExamples[name].call null, options
fixturePath: (file) -> "#{@options.fixturesRoot}/#{file}"
fixture: (file, options={}) ->
@notInsideIt 'fixture'
name = options.as or 'fixture'
env = this
envOptions = @options
ext = file.split('.')[-1..][0]
@before (async) ->
p = env.fixturePath file
envOptions.loadFile(p)
.then (fileContent) =>
env.handleFixture(ext, fileContent).then (result) =>
@[name] = result
async.resolve()
.fail (reason) ->
async.reject reason
registerFixtureHandler: (ext, proc) ->
@fixtureHandlers ||= {}
@fixtureHandlers[ext] = proc
handleFixture: (ext, content) ->
if ext of @fixtureHandlers
@fixtureHandlers[ext].call this, content
else
spectacular.Promise.unit(content)
handleJSONFixture: (content) ->
spectacular.Promise.unit JSON.parse content
handleDOMFixture: (content) ->
spectacular.Promise.unit new spectacular.dom.DOMExpression content
handleHTMLFixture: (content) ->
parent = document.querySelector '#fixtures'
unless parent?
body = document.querySelector('body')
fixtures = document.createElement('div')
fixtures.id = 'fixtures'
body.appendChild fixtures
parent = document.querySelector '#fixtures'
parent.innerHTML = content
@currentExample.ownAfterHooks.push -> parent.innerHTML = ''
spectacular.Promise.unit parent.childNodes
toString: -> '[spectacular Environment]'