forked from HabitRPG/habitica
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathuser.mocha.coffee
240 lines (194 loc) · 8.73 KB
/
user.mocha.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
{expect} = require 'derby/node_modules/racer/test/util'
{BrowserModel: Model} = require 'derby/node_modules/racer/test/util/model'
derby = require 'derby'
clone = require 'clone'
_ = require 'underscore'
moment = require 'moment'
# Custom modules
scoring = require '../src/app/scoring'
schema = require '../src/app/schema'
###### Helpers & Variables ######
model = null
uuid = null
taskPath = null
## Helper which clones the content at a path so tests can compare before/after values
# Otherwise, using model.get(path) will give the same object before as after
pathSnapshots = (paths) ->
if _.isString(paths)
return clone(model.get(paths))
_.map paths, (path) -> clone(model.get(path))
statsTask = -> pathSnapshots(['_user.stats', taskPath]) # quick snapshot of user.stats & task
cleanUserObj = ->
userObj = schema.newUserObject()
userObj.tasks = {}
userObj.habitIds = []
userObj.dailyIds = []
userObj.todoIds = []
userObj.rewardIds = []
return userObj
resetUser = -> model.set '_user', cleanUserObj()
freshTask = (taskObj) ->
resetUser()
# create a test task
uuid = derby.uuid()
taskPath = "_user.tasks.#{uuid}"
{type} = taskObj
model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids"
[taskObj.id, taskObj.value] = [uuid, 0]
model.at("_#{type}List").push taskObj
###
Helper function to determine if stats updates are numerically correct based on scoring
@direction: 'up' or 'down'
@options: The user stats modifiers and times to run, defaults to {times:1, modifiers:{lvl:1, weapon:0, armor:0}}
###
modificationsLookup = (direction, options = {}) ->
merged = _.defaults options, {times:1, lvl:1, weapon:0, armor:0}
{times, lvl, armor, weapon} = merged
userObj = cleanUserObj()
value = 0
_.times times, (n) ->
delta = scoring.taskDeltaFormula(value, direction)
value += delta
if direction=='up'
gain = scoring.expModifier(delta, options)
userObj.stats.exp += gain
userObj.stats.money += gain
else
loss = scoring.hpModifier(delta, options)
userObj.stats.hp += loss
return {user:userObj, value:value}
###### Specs ######
describe 'User', ->
model = null
before ->
model = new Model
model.set '_user', schema.newUserObject()
scoring.setModel model
it 'sets correct user defaults', ->
user = model.get '_user'
expect(user.stats).to.eql { money: 0, exp: 0, lvl: 1, hp: 50 }
expect(user.items).to.eql { itemsEnabled: false, armor: 0, weapon: 0 }
expect(user.balance).to.eql 2
expect(_.size(user.tasks)).to.eql 9
expect(_.size(user.habitIds)).to.eql 3
expect(_.size(user.dailyIds)).to.eql 3
expect(_.size(user.todoIds)).to.eql 1
expect(_.size(user.rewardIds)).to.eql 2
##### Habits #####
describe 'Tasks', ->
beforeEach ->
resetUser()
describe 'Habits', ->
beforeEach ->
freshTask {type: 'habit', text: 'Habit', up: true, down: true}
it 'created the habit', ->
task = model.get(taskPath)
expect(task.text).to.eql 'Habit'
expect(task.value).to.eql 0
it 'test a few scoring numbers (this will change if constants / formulae change)', ->
{user} = modificationsLookup('down')
expect(user.stats.hp).to.eql 49
{user} = modificationsLookup('down', {times:5})
expect(user.stats.hp).to.be.within(42,44)
{user} = modificationsLookup('up')
expect(user.stats.exp).to.eql 1
expect(user.stats.money).to.eql 1
{user} = modificationsLookup('up', {times:5})
expect(user.stats.exp).to.be.within(4,5)
it 'made proper modifications when down-scored', ->
## Trial 1
shouldBe = modificationsLookup('down')
scoring.score(uuid,'down')
[stats, task] = statsTask()
expect(stats.hp).to.be.eql shouldBe.user.stats.hp
expect(task.value).to.eql shouldBe.value
## Trial 2
freshTask {type: 'habit', text: 'Habit', completed: false}
shouldBe = modificationsLookup('down', {times:10})
scoring.score(uuid,'down', {times:10})
[stats, task] = statsTask()
expect(stats.hp).to.be.eql shouldBe.user.stats.hp
expect(task.value).to.eql shouldBe.value
it 'made proper modifications when up-scored', ->
# Up-score the habit
[statsBefore, taskBefore] = statsTask()
scoring.score(uuid, 'up')
[statsAfter, taskAfter] = statsTask()
# User should have gained Exp, GP
expect(statsAfter.exp).to.be.greaterThan statsBefore.exp
expect(statsAfter.money).to.be.greaterThan statsBefore.money
# HP should not change
expect(statsAfter.hp).to.eql statsBefore.hp
# Task should have lost value
expect(taskBefore.value).to.eql 0
expect(taskAfter.value).to.be.greaterThan taskBefore.value
## Trial 2
taskBefore = pathSnapshots(taskPath)
scoring.score(uuid, 'up')
taskAfter = pathSnapshots(taskPath)
# Should have lost in value
expect(taskAfter.value).to.be > taskBefore.value
# And lost more than trial 1
diff = Math.abs(taskAfter.value) - Math.abs(taskBefore.value)
expect(diff).to.be.lessThan 1
it 'makes history entry for habit'
it 'makes proper modifications each time when clicking + / - in rapid succession'
# saw an issue here once, so test that it wasn't a fluke
it 'should not modify certain attributes given certain conditions'
# non up+down habits
# what else?
it 'should show "undo" notification if user unchecks completed daily'
describe 'Lvl & Items', ->
beforeEach ->
freshTask {type: 'habit', text: 'Habit', up: true, down: true}
it 'modified damage based on lvl & armor'
it 'modified exp/gp based on lvl & weapon'
it 'always decreases hp with damage, regardless of stats/items'
it 'always increases exp/gp with gain, regardless of stats/items'
describe 'Dailies', ->
beforeEach ->
freshTask {type: 'daily', text: 'Daily', completed: false}
it 'created the daily', ->
task = model.get(taskPath)
expect(task.text).to.eql 'Daily'
expect(task.value).to.eql 0
it 'does proper calculations when daily is complete'
it 'calculates dailys properly when they have repeat dates'
runCron = (times, pass=1) ->
# Set lastCron to days ago
today = new moment()
ago = new moment().subtract('days',times)
model.set '_user.lastCron', ago.toDate()
# Run run
scoring.cron()
[stats, task] = statsTask()
# Should have updated cron to today
lastCron = moment(model.get('_user.lastCron'))
expect(today.diff(lastCron, 'days')).to.eql 0
shouldBe = modificationsLookup('down', {times:times*pass})
# Should have updated points properly
expect(Math.round(stats.hp)).to.be.eql Math.round(shouldBe.user.stats.hp)
expect(Math.round(task.value)).to.eql Math.round(shouldBe.value)
it 'calculates user.stats & task.value properly on cron', ->
runCron(10)
it 'runs cron multiple times properly', ->
runCron(5)
runCron(5, 2)
#TODO clicking repeat dates on newly-created item doesn't refresh until you refresh the page
#TODO dates on dailies is having issues, possibility: date cusps? my saturday exempts were set to exempt at 8pm friday
describe 'Todos', ->
describe 'Cron', ->
it 'calls cron asyncronously'
it 'should calculate user.stats & task.value properly on cron'
it 'should calculate cron based on difference between start-of-days, and not run in the middle of the day'
it 'should only run set operations once per task, even when daysPassed > 1'
# pass in daysPassed to score, multiply modification values by daysPassed before running set
it 'should only push a history point for lastCron, not each day in between'
# stop passing in tallyFor, let moment().sod().toDate() be handled in scoring.score()
it 'should defer saving user modifications until, save as aggregate values'
# pass in commit parameter to scoring func, if true save right away, otherwise return aggregated array so can save in the end (so total hp loss, etc)
describe 'Rewards', ->
#### Require.js stuff, might be necessary to place in casper.coffee
it "doesn't setup dependent functions until their modules are loaded, require.js callback"
# sortable, stripe, etc
#TODO refactor as user->habits, user->dailys, user->todos, user->rewards