393bbb1885
This commit also sets hoursAgo for many of the cases and restructures how cronMatrix is parsed so that more specific settings may override defaults higher up on the tree.
505 lines
19 KiB
CoffeeScript
505 lines
19 KiB
CoffeeScript
_ = require 'lodash'
|
|
expect = require 'expect.js'
|
|
sinon = require 'sinon'
|
|
moment = require 'moment'
|
|
shared = require '../script/index.coffee'
|
|
|
|
### Helper Functions ####
|
|
newUser = (addTasks=true)->
|
|
buffs = {per:0, int:0, con:0, str:0, stealth: 0, streaks: false}
|
|
user =
|
|
auth:
|
|
timestamps: {}
|
|
stats: {str:1, con:1, per:1, int:1, mp: 32, class: 'warrior', buffs: buffs}
|
|
items:
|
|
lastDrop:
|
|
count: 0
|
|
hatchingPotions: {}
|
|
eggs: {}
|
|
food: {}
|
|
gear:
|
|
equipped: {}
|
|
costume: {}
|
|
party:
|
|
quest:
|
|
progress:
|
|
down: 0
|
|
preferences: {}
|
|
dailys: []
|
|
todos: []
|
|
rewards: []
|
|
flags: {}
|
|
achievements: {}
|
|
|
|
shared.wrap(user)
|
|
user.ops.reset(null, ->)
|
|
if addTasks
|
|
_.each ['habit', 'todo', 'daily'], (task)->
|
|
user.ops.addTask {body: {type: task, id: shared.uuid()}}
|
|
user
|
|
|
|
rewrapUser = (user)->
|
|
user._wrapped = false
|
|
shared.wrap(user)
|
|
user
|
|
|
|
expectStrings = (obj, paths) ->
|
|
_.each paths, (path) -> expect(obj[path]).to.be.ok()
|
|
|
|
# options.hoursAgo: hours since the last cron was executed
|
|
beforeAfter = (options={}) ->
|
|
user = newUser()
|
|
[before, after] = [user, _.cloneDeep(user)]
|
|
# avoid closure on the original user
|
|
rewrapUser(after)
|
|
before.preferences.dayStart = after.preferences.dayStart = options.dayStart if options.dayStart
|
|
before.preferences.timezoneOffset = after.preferences.timezoneOffset = (options.timezoneOffset or moment().zone())
|
|
if options.limitOne
|
|
before["#{options.limitOne}s"] = [before["#{options.limitOne}s"][0]]
|
|
after["#{options.limitOne}s"] = [after["#{options.limitOne}s"][0]]
|
|
lastCron = +(moment(options.now || +new Date).subtract('hours', options.hoursAgo)) unless options.hoursAgo == undefined
|
|
_.each [before,after], (obj) ->
|
|
obj.lastCron = lastCron unless options.hoursAgo == undefined
|
|
{before:before, after:after}
|
|
#TODO calculate actual poins
|
|
|
|
expectLostPoints = (before, after, taskType) ->
|
|
if taskType in ['daily','habit']
|
|
expect(after.stats.hp).to.be.lessThan before.stats.hp
|
|
expect(after["#{taskType}s"][0].history).to.have.length(1)
|
|
else expect(after.history.todos).to.have.length(1)
|
|
expect(after.stats.exp).to.be 0
|
|
expect(after.stats.gp).to.be 0
|
|
expect(after["#{taskType}s"][0].value).to.be.lessThan before["#{taskType}s"][0].value
|
|
|
|
expectGainedPoints = (before, after, taskType) ->
|
|
expect(after.stats.hp).to.be 50
|
|
expect(after.stats.exp).to.be.greaterThan before.stats.exp
|
|
expect(after.stats.gp).to.be.greaterThan before.stats.gp
|
|
expect(after["#{taskType}s"][0].value).to.be.greaterThan before["#{taskType}s"][0].value
|
|
expect(after["#{taskType}s"][0].history).to.have.length(1) if taskType is 'habit'
|
|
# daily & todo histories handled on cron
|
|
|
|
expectNoChange = (before,after) ->
|
|
_.each ['stats', 'items', 'gear', 'dailys', 'todos', 'rewards', 'flags', 'preferences'], (attr)->
|
|
expect(after[attr]).to.eql before[attr]
|
|
|
|
expectDayResetNoDamage = (b,a) ->
|
|
[before,after] = [_.cloneDeep(b); _.cloneDeep(a)]
|
|
_.each after.dailys, (task,i) ->
|
|
expect(task.completed).to.be false
|
|
expect(before.dailys[i].value).to.be task.value
|
|
expect(before.dailys[i].streak).to.be task.streak
|
|
expect(task.history).to.have.length(1)
|
|
_.each after.todos, (task,i) ->
|
|
expect(task.completed).to.be false
|
|
expect(before.todos[i].value).to.be.greaterThan task.value
|
|
expect(after.history.todos).to.have.length(1)
|
|
# hack so we can compare user before/after obj equality sans effected paths
|
|
_.each ['dailys','todos','history','lastCron'], (path) ->
|
|
_.each [before,after], (obj) -> delete obj[path]
|
|
delete after._tmp
|
|
expectNoChange(before, after)
|
|
|
|
cycle = (array)->
|
|
n = -1
|
|
(seed=0)->
|
|
n++
|
|
return array[n % array.length]
|
|
|
|
repeatWithoutLastWeekday = ()->
|
|
repeat = {su:1,m:1,t:1,w:1,th:1,f:1,s:1}
|
|
if shared.startOfWeek(moment().zone(0)).isoWeekday() == 1 # Monday
|
|
repeat.su = false
|
|
else
|
|
repeat.s = false
|
|
{repeat: repeat}
|
|
|
|
###### Specs ######
|
|
|
|
describe 'User', ->
|
|
it 'sets correct user defaults', ->
|
|
user = newUser()
|
|
base_gear = { armor: 'armor_base_0', weapon: 'weapon_base_0', head: 'head_base_0', shield: 'shield_base_0' }
|
|
buffs = {per:0, int:0, con:0, str:0, stealth: 0, streaks: false}
|
|
expect(user.stats).to.eql { str: 1, con: 1, per: 1, int: 1, hp: 50, mp: 32, lvl: 1, exp: 0, gp: 0, class: 'warrior', buffs: buffs }
|
|
expect(user.items.gear).to.eql { equipped: base_gear, costume: base_gear, owned: {weapon_warrior_0: true} }
|
|
expect(user.preferences).to.eql { costume: false }
|
|
|
|
it 'calculates max MP', ->
|
|
user = newUser()
|
|
expect(user._statsComputed.maxMP).to.eql 32
|
|
user.stats.int = 10
|
|
expect(user._statsComputed.maxMP).to.eql 50
|
|
user.stats.lvl = 5
|
|
expect(user._statsComputed.maxMP).to.eql 54
|
|
user.stats.class = 'wizard'
|
|
user.items.gear.equipped.weapon = 'weapon_wizard_1'
|
|
expect(user._statsComputed.maxMP).to.eql 63
|
|
|
|
describe 'Death', ->
|
|
user = undefined
|
|
it 'revives correctly', ->
|
|
user = newUser()
|
|
user.stats = { gp: 10, exp: 100, lvl: 2, hp: 1 }
|
|
user.ops.revive()
|
|
expect(user.stats.gp).to.eql 0
|
|
expect(user.stats.exp).to.eql 0
|
|
expect(user.stats.lvl).to.eql 1
|
|
expect(user.stats.hp).to.eql 50
|
|
expect(user.items.gear.owned).to.eql { weapon_warrior_0: false }
|
|
|
|
it "doesn't break unbreakables", ->
|
|
ce = shared.countExists
|
|
user = newUser()
|
|
user.items.gear.owned['shield_rogue_1'] = true
|
|
user.items.gear.owned['head_special_nye'] = true
|
|
expect(ce user.items.gear.owned).to.be 3
|
|
user.ops.revive()
|
|
expect(ce(user.items.gear.owned)).to.be 2
|
|
user.ops.revive()
|
|
expect(ce(user.items.gear.owned)).to.be 1
|
|
user.ops.revive()
|
|
expect(ce(user.items.gear.owned)).to.be 0
|
|
|
|
it "handles event items", ->
|
|
shared.content.gear.flat.head_special_nye.event.start = '2012-01-01'
|
|
shared.content.gear.flat.head_special_nye.event.end = '2012-02-01'
|
|
expect(shared.content.gear.flat.head_special_nye.canOwn(user)).to.be true
|
|
delete user.items.gear.owned['head_special_nye']
|
|
expect(shared.content.gear.flat.head_special_nye.canOwn(user)).to.be false
|
|
|
|
shared.content.gear.flat.head_special_nye.event.start = moment().subtract('days',5)
|
|
shared.content.gear.flat.head_special_nye.event.end = moment().add('days',5)
|
|
expect(shared.content.gear.flat.head_special_nye.canOwn(user)).to.be true
|
|
|
|
|
|
describe 'store', ->
|
|
it 'recovers hp buying potions', ->
|
|
user = newUser()
|
|
user.stats.hp = 30
|
|
user.stats.gp = 50
|
|
user.ops.buy {params: {key: 'potion'}}
|
|
expect(user.stats.hp).to.eql 45
|
|
expect(user.stats.gp).to.eql 25
|
|
|
|
user.ops.buy {params: {key: 'potion'}}
|
|
expect(user.stats.hp).to.eql 50 # don't exceed max hp
|
|
expect(user.stats.gp).to.eql 0
|
|
|
|
it 'buys equipment', ->
|
|
user = newUser()
|
|
user.stats.gp = 31
|
|
user.ops.buy {params: {key: 'armor_warrior_1'}}
|
|
expect(user.items.gear.owned).to.eql { weapon_warrior_0: true, armor_warrior_1: true }
|
|
expect(user.items.gear.equipped).to.eql { armor: 'armor_warrior_1', weapon: 'weapon_base_0', head: 'head_base_0', shield: 'shield_base_0' }
|
|
expect(user.stats.gp).to.eql 1
|
|
|
|
it 'do not buy equipment without enough money', ->
|
|
user = newUser()
|
|
user.stats.gp = 1
|
|
user.ops.buy {params: {key: 'armor_warrior_1'}}
|
|
expect(user.items.gear.equipped).to.eql { armor: 'armor_base_0', weapon: 'weapon_base_0', head: 'head_base_0', shield: 'shield_base_0' }
|
|
expect(user.stats.gp).to.eql 1
|
|
|
|
describe 'drop system', ->
|
|
user = null
|
|
|
|
beforeEach ->
|
|
user = newUser()
|
|
user.flags.dropsEnabled = true
|
|
# too many predictableRandom calls to stub, let's return the last element
|
|
sinon.stub(user.fns, 'randomVal', (obj)->
|
|
result = undefined
|
|
for key, val of obj
|
|
result = val
|
|
result
|
|
)
|
|
@task_id = shared.uuid()
|
|
user.ops.addTask({body: {type: 'daily', id: @task_id}})
|
|
|
|
it 'gets a golden potion', ->
|
|
sinon.stub(user.fns, 'predictableRandom').returns 0
|
|
user.ops.score {params: { id: @task_id, direction: 'up'}}
|
|
expect(user.items.eggs).to.eql {}
|
|
expect(user.items.hatchingPotions).to.eql {'Golden': 1}
|
|
expect(user.items.food).to.eql {}
|
|
|
|
it 'gets a bear cub egg', ->
|
|
sinon.stub(user.fns, 'predictableRandom', cycle [0, 0, 0.55])
|
|
user.ops.score {params: { id: @task_id, direction: 'up'}}
|
|
expect(user.items.eggs).to.eql {'BearCub': 1}
|
|
expect(user.items.hatchingPotions).to.eql {}
|
|
expect(user.items.food).to.eql {}
|
|
|
|
it 'gets honey', ->
|
|
sinon.stub(user.fns, 'predictableRandom', cycle [0, 0, 0.9])
|
|
user.ops.score {params: { id: @task_id, direction: 'up'}}
|
|
expect(user.items.eggs).to.eql {}
|
|
expect(user.items.hatchingPotions).to.eql {}
|
|
expect(user.items.food).to.eql {'Honey': 1}
|
|
|
|
it 'does not get a drop', ->
|
|
sinon.stub(user.fns, 'predictableRandom').returns 0.5
|
|
user.ops.score {params: { id: @task_id, direction: 'up'}}
|
|
expect(user.items.eggs).to.eql {}
|
|
expect(user.items.hatchingPotions).to.eql {}
|
|
expect(user.items.food).to.eql {}
|
|
|
|
afterEach ->
|
|
user.fns.randomVal.restore()
|
|
user.fns.predictableRandom.restore()
|
|
|
|
describe 'Simple Scoring', ->
|
|
beforeEach ->
|
|
{@before, @after} = beforeAfter()
|
|
|
|
it 'Habits : Up', ->
|
|
@after.ops.score {params: {id: @after.habits[0].id, direction: 'down'}, query: {times: 5}}
|
|
expectLostPoints(@before, @after,'habit')
|
|
|
|
it 'Habits : Down', ->
|
|
@after.ops.score {params: {id: @after.habits[0].id, direction: 'up'}, query: {times: 5}}
|
|
expectGainedPoints(@before, @after,'habit')
|
|
|
|
it 'Dailys : Up', ->
|
|
@after.ops.score {params: {id: @after.dailys[0].id, direction: 'up'}}
|
|
expectGainedPoints(@before, @after,'daily')
|
|
|
|
it 'Todos : Up', ->
|
|
@after.ops.score {params: {id: @after.todos[0].id, direction: 'up'}}
|
|
expectGainedPoints(@before, @after,'todo')
|
|
|
|
describe 'Cron', ->
|
|
|
|
it 'computes shouldCron', ->
|
|
user = newUser()
|
|
|
|
paths = {};user.fns.cron {paths}
|
|
expect(user.lastCron).to.not.be.ok # it setup the cron property now
|
|
|
|
user.lastCron = +moment().subtract('days',1)
|
|
paths = {};user.fns.cron {paths}
|
|
expect(user.lastCron).to.be.greaterThan 0
|
|
|
|
# user.lastCron = +moment().add('days',1)
|
|
# paths = {};algos.cron user, {paths}
|
|
# expect(paths.lastCron).to.be true # busted cron (was set to after today's date)
|
|
|
|
it 'only dailies & todos are effected', ->
|
|
{before,after} = beforeAfter({hoursAgo:24})
|
|
before.dailys = before.todos = after.dailys = after.todos = []
|
|
after.fns.cron()
|
|
expect(after.lastCron).to.not.be before.lastCron # make sure cron was run
|
|
expect(before.stats).to.eql after.stats
|
|
beforeTasks = before.habits.concat(before.dailys).concat(before.todos).concat(before.rewards)
|
|
afterTasks = after.habits.concat(after.dailys).concat(after.todos).concat(after.rewards)
|
|
expect(beforeTasks).to.eql afterTasks
|
|
|
|
describe 'preening', ->
|
|
beforeEach ->
|
|
@clock = sinon.useFakeTimers(Date.parse("2013-11-20"), "Date");
|
|
|
|
afterEach ->
|
|
@clock.restore()
|
|
|
|
it 'should preen user history', ->
|
|
{before,after} = beforeAfter({hoursAgo:24})
|
|
history = [
|
|
# Last year should be condensed to one entry, avg: 1
|
|
{date:'09/01/2012', value: 0}
|
|
{date:'10/01/2012', value: 0}
|
|
{date:'11/01/2012', value: 2}
|
|
{date:'12/01/2012', value: 2}
|
|
|
|
# Each month of this year should be condensed to 1/mo, averages follow
|
|
{date:'01/01/2013', value: 1} #2
|
|
{date:'01/15/2013', value: 3}
|
|
|
|
{date:'02/01/2013', value: 2} #3
|
|
{date:'02/15/2013', value: 4}
|
|
|
|
{date:'03/01/2013', value: 3} #4
|
|
{date:'03/15/2013', value: 5}
|
|
|
|
{date:'04/01/2013', value: 4} #5
|
|
{date:'04/15/2013', value: 6}
|
|
|
|
{date:'05/01/2013', value: 5} #6
|
|
{date:'05/15/2013', value: 7}
|
|
|
|
{date:'06/01/2013', value: 6} #7
|
|
{date:'06/15/2013', value: 8}
|
|
|
|
{date:'07/01/2013', value: 7} #8
|
|
{date:'07/15/2013', value: 9}
|
|
|
|
{date:'08/01/2013', value: 8} #9
|
|
{date:'08/15/2013', value: 10}
|
|
|
|
{date:'09/01/2013', value: 9} #10
|
|
{date:'09/15/2013', value: 11}
|
|
|
|
{date:'010/01/2013', value: 10} #11
|
|
{date:'010/15/2013', value: 12}
|
|
|
|
# This month should condense each week
|
|
{date:'011/01/2013', value: 12}
|
|
{date:'011/02/2013', value: 13}
|
|
{date:'011/03/2013', value: 14}
|
|
{date:'011/04/2013', value: 15}
|
|
]
|
|
after.history = {exp: _.cloneDeep(history), todos: _.cloneDeep(history)}
|
|
after.habits[0].history = _.cloneDeep(history)
|
|
after.fns.cron()
|
|
|
|
# remove history entries created by cron
|
|
after.history.exp.pop()
|
|
after.history.todos.pop()
|
|
|
|
_.each [after.history.exp, after.history.todos, after.habits[0].history], (arr) ->
|
|
expect(_.map(arr, (x)->x.value)).to.eql [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
|
|
|
|
describe 'Todos', ->
|
|
it '1 day missed', ->
|
|
{before,after} = beforeAfter({hoursAgo:24})
|
|
before.dailys = after.dailys = []
|
|
after.fns.cron()
|
|
|
|
# todos don't effect stats
|
|
expect(after.stats.hp).to.be 50
|
|
expect(after.stats.exp).to.be 0
|
|
expect(after.stats.gp).to.be 0
|
|
|
|
# but they devalue
|
|
expect(after.todos[0].value).to.be.lessThan before.todos[0].value
|
|
expect(after.history.todos).to.have.length 1
|
|
|
|
describe 'dailies', ->
|
|
|
|
describe 'new day', ->
|
|
|
|
###
|
|
This section runs through a "cron matrix" of all permutations (that I can easily account for). It sets
|
|
task due days, user custom day start, timezoneOffset, etc - then runs cron, jumps to tomorrow and runs cron,
|
|
and so on - testing each possible outcome along the way
|
|
###
|
|
|
|
runCron = (options) ->
|
|
_.each [480, 240, 0, -120], (timezoneOffset) -> # test different timezones
|
|
now = shared.startOfWeek({timezoneOffset}).add('hours', options.currentHour||0)
|
|
{before,after} = beforeAfter({now, timezoneOffset, hoursAgo:options.hoursAgo||0, dayStart:options.dayStart||0, limitOne:'daily'})
|
|
before.dailys[0].repeat = after.dailys[0].repeat = options.repeat if options.repeat
|
|
before.dailys[0].streak = after.dailys[0].streak = 10
|
|
before.dailys[0].completed = after.dailys[0].completed = true if options.checked
|
|
if options.shouldDo
|
|
expect(shared.shouldDo(now, options.repeat, {timezoneOffset, dayStart:options.dayStart, now})).to.be.ok()
|
|
after.fns.cron {now}
|
|
switch options.expect
|
|
when 'losePoints' then expectLostPoints(before,after,'daily')
|
|
when 'noChange' then expectNoChange(before,after)
|
|
when 'noDamage' then expectDayResetNoDamage(before,after)
|
|
{before,after}
|
|
|
|
cronMatrix =
|
|
steps:
|
|
|
|
'due yesterday':
|
|
defaults: {hoursAgo:24, limitOne: 'daily'}
|
|
steps:
|
|
|
|
'(simple)': {expect:'losePoints'}
|
|
|
|
'due today':
|
|
# NOTE: a strange thing here, moment().startOf('week') is Sunday, but moment.zone(myTimeZone).startOf('week') is Monday.
|
|
defaults: {repeat:{su:1,m:true,t:1,w:1,th:1,f:1,s:1}}
|
|
steps:
|
|
'pre-dayStart':
|
|
defaults: {hoursAgo:2,currentHour:3, dayStart:4, shouldDo:true}
|
|
steps:
|
|
'checked': {checked: true, expect:'noChange'}
|
|
'un-checked': {checked: false, expect:'noChange'}
|
|
'post-dayStart':
|
|
defaults: {currentHour:5, dayStart:4, shouldDo:true}
|
|
steps:
|
|
'checked': {checked:true, expect:'noDamage'}
|
|
'unchecked': {checked:false, expect: 'losePoints'}
|
|
|
|
'NOT due today':
|
|
defaults: {repeat:{su:1,m:false,t:1,w:1,th:1,f:1,s:1}}
|
|
steps:
|
|
'pre-dayStart':
|
|
defaults: {hoursAgo:23, currentHour:3, dayStart:4, shouldDo:true}
|
|
steps:
|
|
'checked': {checked: true, expect:'noChange'}
|
|
'un-checked': {checked: false, expect:'noChange'}
|
|
'post-dayStart':
|
|
defaults: {currentHour:5, dayStart:4, shouldDo:false}
|
|
steps:
|
|
'checked': {checked:true, expect:'noDamage'}
|
|
'unchecked': {checked:false, expect: 'losePoints'}
|
|
|
|
'not due yesterday':
|
|
defaults: repeatWithoutLastWeekday()
|
|
steps:
|
|
'(simple)': {hoursAgo:24, expect:'noDamage'}
|
|
'post-dayStart': {hoursAgo:23,currentHour:5,dayStart:4, expect:'noDamage'}
|
|
'pre-dayStart': {hoursAgo:21, currentHour:3, dayStart:4, expect:'noChange'}
|
|
|
|
recurseCronMatrix = (obj, options={}) ->
|
|
if obj.steps
|
|
_.each obj.steps, (step, text) ->
|
|
o = _.cloneDeep options
|
|
o.text ?= ''; o.text += " #{text} "
|
|
recurseCronMatrix step, _.assign(o, obj.defaults)
|
|
else
|
|
it "#{options.text}", -> runCron(_.assign(options, obj))
|
|
recurseCronMatrix(cronMatrix)
|
|
|
|
it 'calculates day differences with dayStart properly', ->
|
|
dayStart = 4
|
|
yesterday = shared.startOfDay {now: moment().subtract('d', 1), dayStart}
|
|
now = shared.startOfDay {dayStart: dayStart-1}
|
|
expect(shared.daysSince(yesterday, {now, dayStart})).to.eql 0
|
|
now = moment().startOf('day').add('h', dayStart).add('m', 1)
|
|
expect(shared.daysSince(yesterday, {now, dayStart})).to.eql 1
|
|
|
|
describe 'Helper', ->
|
|
it 'calculates gold coins', ->
|
|
expect(shared.gold(10)).to.eql 10
|
|
expect(shared.gold(1.957)).to.eql 1
|
|
expect(shared.gold()).to.eql 0
|
|
|
|
it 'calculates silver coins', ->
|
|
expect(shared.silver(10)).to.eql 0
|
|
expect(shared.silver(1.957)).to.eql 95
|
|
expect(shared.silver(0.01)).to.eql "01"
|
|
expect(shared.silver()).to.eql "00"
|
|
|
|
it 'calculates experience to next level', ->
|
|
expect(shared.tnl 1).to.eql 150
|
|
expect(shared.tnl 2).to.eql 160
|
|
expect(shared.tnl 10).to.eql 260
|
|
expect(shared.tnl 99).to.eql 3580
|
|
expect(shared.tnl 100).to.eql 0
|
|
|
|
it 'calculates the start of the day', ->
|
|
expect(shared.startOfDay({now: new Date(2013, 0, 1, 0)}).format('YYYY-MM-DD HH:mm')).to.eql '2013-01-01 00:00'
|
|
expect(shared.startOfDay({now: new Date(2013, 0, 1, 5)}).format('YYYY-MM-DD HH:mm')).to.eql '2013-01-01 00:00'
|
|
expect(shared.startOfDay({now: new Date(2013, 0, 1, 23, 59, 59)}).format('YYYY-MM-DD HH:mm')).to.eql '2013-01-01 00:00'
|
|
# between midnight and custom day start
|
|
expect(shared.startOfDay({now: new Date(2013, 0, 2, 2), dayStart: 4}).format('YYYY-MM-DD HH:mm')).to.eql '2013-01-01 04:00'
|
|
|
|
it 'counts pets', ->
|
|
pets = {}
|
|
expect(shared.countPets(null, pets)).to.eql 0
|
|
expect(shared.countPets(1, pets)).to.eql 1
|
|
|
|
pets = { "Dragon-Red": 1, "Wolf-Base": 2 }
|
|
expect(shared.countPets(null, pets)).to.eql 2
|
|
expect(shared.countPets(2, pets)).to.eql 2
|
|
|
|
pets = { "Wolf-Base": 2, "Wolf-Veteran": 1, "Wolf-Cerberus": 1, "Dragon-Hydra": 1}
|
|
expect(shared.countPets(null, pets)).to.eql 1
|
|
expect(shared.countPets(_.size(pets), pets)).to.eql 1
|