Compare commits
239 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ed1353e31 | |||
| c748477546 | |||
| d5517ffc5f | |||
| 7b6f63958a | |||
| a2cd79f20e | |||
| 6a367d3697 | |||
| ef0b19f17e | |||
| a6e96f7ad9 | |||
| 43b8368f42 | |||
| 5362058f35 | |||
| 9d6fb2ca26 | |||
| 9213ee92de | |||
| 3a80875505 | |||
| 2d6802ae87 | |||
| 497c023995 | |||
| b97d514c68 | |||
| 142fdfe743 | |||
| ee5a2b0e36 | |||
| 9455f996ef | |||
| 5cc3f6e8aa | |||
| 8bad3d1184 | |||
| 3f65353974 | |||
| ab7c4015a2 | |||
| 6d509ae1f8 | |||
| 6375bc1d59 | |||
| 14714f9e1c | |||
| 60b3f2b860 | |||
| 965df326ed | |||
| 3c49db9bfe | |||
| 72bd104567 | |||
| 2cbd376cf6 | |||
| a7477e137e | |||
| f6cb57c22f | |||
| 76fa4dfd7f | |||
| aceaeacbf0 | |||
| b2cb5f83de | |||
| d09dea5185 | |||
| 6fa7fbce3f | |||
| b17c9a33a5 | |||
| 31d0cb5a91 | |||
| d678eb920f | |||
| 163eb55dbb | |||
| 8661716c23 | |||
| ea11e302c6 | |||
| afdd5af7a9 | |||
| a09e56048e | |||
| fd37ee90da | |||
| a4cfb97dbf | |||
| ea766251c2 | |||
| f196ff2e24 | |||
| cc901fe085 | |||
| f257488b39 | |||
| 48dbe547c0 | |||
| b15462596b | |||
| 2a98b5b7bf | |||
| f7667fcf79 | |||
| 05aaad8743 | |||
| 5e1f2c16f8 | |||
| 82b6a14d5b | |||
| f9cfd9fb5e | |||
| 60bef56577 | |||
| af87185bfa | |||
| f8c8be4f4c | |||
| 5d220544e0 | |||
| c4fd9daa90 | |||
| fc8f9cbaa0 | |||
| e39d3e52e2 | |||
| 625b4a4ad7 | |||
| f2bcdd21de | |||
| ad51675ac6 | |||
| 869d2df4fa | |||
| 734e997345 | |||
| 4fc260e552 | |||
| fa22a47b7a | |||
| 0366245fab | |||
| f342eff70b | |||
| 84eb39fde2 | |||
| d6fe2c76e2 | |||
| f19e9dd57e | |||
| 13566e8a39 | |||
| 7d3dd9f157 | |||
| 92f283f6a2 | |||
| 5d24d584d4 | |||
| 0320827f7e | |||
| 79c1a5d9c1 | |||
| faa49b1412 | |||
| c3decc3951 | |||
| 084adf8b0d | |||
| 04574dad69 | |||
| 6526a6317e | |||
| c778e5e84e | |||
| b5dacdf9ea | |||
| e05d1dae43 | |||
| 864db644e3 | |||
| 5ddd4ec564 | |||
| 2d7d4af2b8 | |||
| bad3f82dfb | |||
| 8595641d12 | |||
| 9bb3e17995 | |||
| 8b955e2c5e | |||
| 1d7e02428b | |||
| eee04255f8 | |||
| 7f30385c09 | |||
| 5b6eeef290 | |||
| 9953c9346d | |||
| d62930b9da | |||
| 67c607216f | |||
| 672fd43ad0 | |||
| 69281f80ea | |||
| 093bcbb715 | |||
| 1f5992ccc5 | |||
| 2d598c9933 | |||
| b4be8286e2 | |||
| 96b985a191 | |||
| a196a0cae2 | |||
| 8f9043e4f1 | |||
| 1cace198a1 | |||
| ec0e5024a7 | |||
| 0275636f46 | |||
| ea7ae4bb2f | |||
| 9f32a879e3 | |||
| 665200b49d | |||
| c63e92c8f2 | |||
| ba5b79855f | |||
| 97051bb3ae | |||
| 29e3cae0a6 | |||
| 274c582af8 | |||
| 4a7f40ea37 | |||
| 97ad0fae26 | |||
| b7b91caef1 | |||
| daa3458760 | |||
| 8e6ce39d64 | |||
| 617832f02d | |||
| 2cfc765e39 | |||
| 43efe2d6d6 | |||
| 9474a44df3 | |||
| 7cd9f800cc | |||
| 9a1cadd49f | |||
| b3285db5b0 | |||
| 8b1c009990 | |||
| 9061a59fc2 | |||
| cbfed9c0d3 | |||
| 77447f7096 | |||
| 361bb92b3b | |||
| e50d5f514c | |||
| a7ac4633a8 | |||
| 6a27feb3f9 | |||
| 31b53fd6ed | |||
| e04d4e8bea | |||
| 3e31223812 | |||
| 2832226539 | |||
| dcbc5da2ba | |||
| 1d44bfe0cd | |||
| 6553118636 | |||
| a238b264e5 | |||
| 4696237b21 | |||
| b7956a82ee | |||
| a5babc493f | |||
| 708bd4a292 | |||
| d9e774dd77 | |||
| 97ef3b1d4b | |||
| 8e3ac280b0 | |||
| 7fedbdde03 | |||
| 6ad808f717 | |||
| c6541399bb | |||
| 81028893b2 | |||
| 943862c0ea | |||
| 43511aacb9 | |||
| ce68e2f855 | |||
| ec06faef32 | |||
| cfac54d44c | |||
| f4b41bd958 | |||
| 7d7d71e95f | |||
| 2b21c2a28e | |||
| dfa8725c55 | |||
| 1d8880c04d | |||
| 07fd3cef4c | |||
| b58443140a | |||
| a0b93d57e4 | |||
| 7cac574c31 | |||
| 86619a2ac9 | |||
| 058c1464d3 | |||
| 70e747c131 | |||
| 8869d3ebf0 | |||
| d82f4f5c91 | |||
| d9a6120cfe | |||
| 5c7ad58ce4 | |||
| 284510d0a3 | |||
| 0791848fa3 | |||
| 466f109edb | |||
| 87fe4aa28e | |||
| baa86bab88 | |||
| 17cbe16773 | |||
| 660f3635aa | |||
| 10a28a9161 | |||
| 2964eff298 | |||
| 7119763c1e | |||
| 12bd10a095 | |||
| 221c95eb14 | |||
| 7815d501fa | |||
| 9dec717e10 | |||
| d23368a826 | |||
| 33913743e9 | |||
| d92b344e91 | |||
| def065d86a | |||
| 5b50761d9f | |||
| 30a52e2bb8 | |||
| b361e3fc82 | |||
| 7419e8a926 | |||
| 0d04509a97 | |||
| 4950ceb814 | |||
| 218d186969 | |||
| 4f6306e748 | |||
| 334478f3d4 | |||
| fd4c50083c | |||
| 435471a48c | |||
| be96e2c3a4 | |||
| d018401f96 | |||
| 63bb9b8e68 | |||
| 682f559762 | |||
| aa39af5ab4 | |||
| d9503ef35a | |||
| 322a826576 | |||
| 362dea6c5c | |||
| f74e908bd5 | |||
| d4c11ff798 | |||
| 24bc3822b6 | |||
| 4fc796b177 | |||
| c099242cb7 | |||
| f7142b6f55 | |||
| c0be28072b | |||
| a218f9ef25 | |||
| 65e881fa5a | |||
| 285507d68a | |||
| 0a2e50ce76 | |||
| 49d8c739c0 | |||
| 2c01e6347d | |||
| d050ef4779 | |||
| e4e980a6e3 |
@@ -0,0 +1,82 @@
|
||||
/* eslint-disable no-console */
|
||||
const MIGRATION_NAME = '20201020_pet_color_achievements';
|
||||
import { model as User } from '../../../website/server/models/user';
|
||||
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
async function updateUser (user) {
|
||||
count++;
|
||||
|
||||
const set = {
|
||||
migration: MIGRATION_NAME,
|
||||
};
|
||||
|
||||
if (user && user.items && user.items.pets) {
|
||||
const pets = user.items.pets;
|
||||
if (pets['Wolf-Golden'] > 0
|
||||
&& pets['TigerCub-Skeleton'] > 0
|
||||
&& pets['PandaCub-Skeleton'] > 0
|
||||
&& pets['LionCub-Skeleton'] > 0
|
||||
&& pets['Fox-Skeleton'] > 0
|
||||
&& pets['FlyingPig-Skeleton'] > 0
|
||||
&& pets['Dragon-Skeleton'] > 0
|
||||
&& pets['Cactus-Skeleton'] > 0
|
||||
&& pets['BearCub-Skeleton'] > 0) {
|
||||
set['achievements.boneCollector'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (user && user.items && user.items.mounts) {
|
||||
const mounts = user.items.mounts;
|
||||
if (mounts['Wolf-Skeleton']
|
||||
&& mounts['TigerCub-Skeleton']
|
||||
&& mounts['PandaCub-Skeleton']
|
||||
&& mounts['LionCub-Skeleton']
|
||||
&& mounts['Fox-Skeleton']
|
||||
&& mounts['FlyingPig-Skeleton']
|
||||
&& mounts['Dragon-Skeleton']
|
||||
&& mounts['Cactus-Skeleton']
|
||||
&& mounts['BearCub-Skeleton'] ) {
|
||||
set['achievements.skeletonCrew'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
|
||||
return await User.update({ _id: user._id }, { $set: set }).exec();
|
||||
}
|
||||
|
||||
module.exports = async function processUsers () {
|
||||
let query = {
|
||||
migration: { $ne: MIGRATION_NAME },
|
||||
'auth.timestamps.loggedin': { $gt: new Date('2020-10-01') },
|
||||
};
|
||||
|
||||
const fields = {
|
||||
_id: 1,
|
||||
items: 1,
|
||||
};
|
||||
|
||||
while (true) { // eslint-disable-line no-constant-condition
|
||||
const users = await User // eslint-disable-line no-await-in-loop
|
||||
.find(query)
|
||||
.limit(250)
|
||||
.sort({_id: 1})
|
||||
.select(fields)
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
if (users.length === 0) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
console.warn(`\n${count} users processed\n`);
|
||||
break;
|
||||
} else {
|
||||
query._id = {
|
||||
$gt: users[users.length - 1]._id,
|
||||
};
|
||||
}
|
||||
|
||||
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Award Habitoween ladder items to participants in this month's Habitoween festivities
|
||||
*/
|
||||
/* eslint-disable no-console */
|
||||
|
||||
const MIGRATION_NAME = '20201029_habitoween_ladder'; // Update when running in future years
|
||||
|
||||
import { model as User } from '../../../website/server/models/user';
|
||||
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
async function updateUser (user) {
|
||||
count++;
|
||||
|
||||
const set = {};
|
||||
const inc = {
|
||||
'items.food.Candy_Skeleton': 1,
|
||||
'items.food.Candy_Base': 1,
|
||||
'items.food.Candy_CottonCandyBlue': 1,
|
||||
'items.food.Candy_CottonCandyPink': 1,
|
||||
'items.food.Candy_Shade': 1,
|
||||
'items.food.Candy_White': 1,
|
||||
'items.food.Candy_Golden': 1,
|
||||
'items.food.Candy_Zombie': 1,
|
||||
'items.food.Candy_Desert': 1,
|
||||
'items.food.Candy_Red': 1,
|
||||
};
|
||||
|
||||
set.migration = MIGRATION_NAME;
|
||||
|
||||
if (user && user.items && user.items.mounts && user.items.mounts['JackOLantern-Glow']) {
|
||||
set['items.pets.JackOLantern-RoyalPurple'] = 5;
|
||||
} else if (user && user.items && user.items.pets && user.items.pets['JackOLantern-Glow']) {
|
||||
set['items.mounts.JackOLantern-Glow'] = true;
|
||||
} else if (user && user.items && user.items.mounts && user.items.mounts['JackOLantern-Ghost']) {
|
||||
set['items.pets.JackOLantern-Glow'] = 5;
|
||||
} else if (user && user.items && user.items.pets && user.items.pets['JackOLantern-Ghost']) {
|
||||
set['items.mounts.JackOLantern-Ghost'] = true;
|
||||
} else if (user && user.items && user.items.mounts && user.items.mounts['JackOLantern-Base']) {
|
||||
set['items.pets.JackOLantern-Ghost'] = 5;
|
||||
} else if (user && user.items && user.items.pets && user.items.pets['JackOLantern-Base']) {
|
||||
set['items.mounts.JackOLantern-Base'] = true;
|
||||
} else {
|
||||
set['items.pets.JackOLantern-Base'] = 5;
|
||||
}
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
return await User.update({_id: user._id}, {$inc: inc, $set: set}).exec();
|
||||
}
|
||||
|
||||
module.exports = async function processUsers () {
|
||||
let query = {
|
||||
migration: {$ne: MIGRATION_NAME},
|
||||
'auth.timestamps.loggedin': {$gt: new Date('2020-10-01')},
|
||||
};
|
||||
|
||||
const fields = {
|
||||
_id: 1,
|
||||
items: 1,
|
||||
};
|
||||
|
||||
while (true) { // eslint-disable-line no-constant-condition
|
||||
const users = await User // eslint-disable-line no-await-in-loop
|
||||
.find(query)
|
||||
.limit(250)
|
||||
.sort({_id: 1})
|
||||
.select(fields)
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
if (users.length === 0) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
console.warn(`\n${count} users processed\n`);
|
||||
break;
|
||||
} else {
|
||||
query._id = {
|
||||
$gt: users[users.length - 1],
|
||||
};
|
||||
}
|
||||
|
||||
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Fix JackOLantern-Base for users that signed up recently
|
||||
*/
|
||||
/* eslint-disable no-console */
|
||||
|
||||
const MIGRATION_NAME = '20201102_fix_habitoween'; // Update when running in future years
|
||||
|
||||
import { model as User } from '../../../website/server/models/user';
|
||||
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
async function updateUser (user) {
|
||||
count++;
|
||||
|
||||
const set = {};
|
||||
|
||||
set.migration = MIGRATION_NAME;
|
||||
set['items.pets.JackOLantern-Base'] = 5;
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
return await User.update({_id: user._id}, {$inc: inc, $set: set}).exec();
|
||||
}
|
||||
|
||||
module.exports = async function processUsers () {
|
||||
let query = {
|
||||
migration: {$ne: MIGRATION_NAME},
|
||||
'auth.timestamps.created': {$gt: new Date('2020-10-26')},
|
||||
'items.pets.JackOLantern-Base': true,
|
||||
};
|
||||
|
||||
const fields = {
|
||||
_id: 1,
|
||||
items: 1,
|
||||
};
|
||||
|
||||
while (true) { // eslint-disable-line no-constant-condition
|
||||
const users = await User // eslint-disable-line no-await-in-loop
|
||||
.find(query)
|
||||
.limit(250)
|
||||
.sort({_id: 1})
|
||||
.select(fields)
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
if (users.length === 0) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
console.warn(`\n${count} users processed\n`);
|
||||
break;
|
||||
} else {
|
||||
query._id = {
|
||||
$gt: users[users.length - 1],
|
||||
};
|
||||
}
|
||||
|
||||
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* All web users should be enrolled in the Drop Cap AB Test
|
||||
*/
|
||||
/* eslint-disable no-console */
|
||||
|
||||
const MIGRATION_NAME = '20201103_drop_cap_ab_tweaks';
|
||||
|
||||
import { model as User } from '../../../website/server/models/user';
|
||||
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
async function updateUser (user) {
|
||||
count++;
|
||||
|
||||
const set = {};
|
||||
|
||||
set.migration = MIGRATION_NAME;
|
||||
|
||||
const testGroup = Math.random();
|
||||
// Enroll 100% of users, splitting them 50/50
|
||||
const value = testGroup <= 0.50 ? 'drop-cap-notif-enabled' : 'drop-cap-notif-disabled';
|
||||
set['_ABtests.dropCapNotif'] = value;
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
return await User.update({_id: user._id}, {$set: set}).exec();
|
||||
}
|
||||
|
||||
module.exports = async function processUsers () {
|
||||
let query = {
|
||||
migration: {$ne: MIGRATION_NAME},
|
||||
'auth.timestamps.loggedin': {$gt: new Date('2020-10-10')},
|
||||
'_ABtests.dropCapNotif': 'drop-cap-notif-not-enrolled',
|
||||
};
|
||||
|
||||
const fields = {
|
||||
_id: 1,
|
||||
_ABtests: 1,
|
||||
};
|
||||
|
||||
while (true) { // eslint-disable-line no-constant-condition
|
||||
const users = await User // eslint-disable-line no-await-in-loop
|
||||
.find(query)
|
||||
.limit(250)
|
||||
.sort({_id: 1})
|
||||
.select(fields)
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
if (users.length === 0) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
console.warn(`\n${count} users processed\n`);
|
||||
break;
|
||||
} else {
|
||||
query._id = {
|
||||
$gt: users[users.length - 1],
|
||||
};
|
||||
}
|
||||
|
||||
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
};
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "4.161.4",
|
||||
"version": "4.169.1",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.11.6",
|
||||
"@babel/preset-env": "^7.11.5",
|
||||
"@babel/register": "^7.11.5",
|
||||
"@babel/core": "^7.12.3",
|
||||
"@babel/preset-env": "^7.12.1",
|
||||
"@babel/register": "^7.12.1",
|
||||
"@google-cloud/trace-agent": "^5.1.1",
|
||||
"@slack/client": "^4.12.0",
|
||||
"accepts": "^1.3.5",
|
||||
@@ -30,7 +30,7 @@
|
||||
"express-basic-auth": "^1.1.5",
|
||||
"express-validator": "^5.2.0",
|
||||
"glob": "^7.1.6",
|
||||
"got": "^11.7.0",
|
||||
"got": "^11.8.0",
|
||||
"gulp": "^4.0.0",
|
||||
"gulp-babel": "^8.0.0",
|
||||
"gulp-imagemin": "^7.1.0",
|
||||
@@ -38,17 +38,17 @@
|
||||
"gulp.spritesmith": "^6.9.0",
|
||||
"habitica-markdown": "^3.0.0",
|
||||
"helmet": "^3.23.3",
|
||||
"image-size": "^0.9.1",
|
||||
"image-size": "^0.9.3",
|
||||
"in-app-purchase": "^1.11.3",
|
||||
"js2xmlparser": "^4.0.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"jwks-rsa": "^1.10.1",
|
||||
"jwks-rsa": "^1.11.0",
|
||||
"lodash": "^4.17.20",
|
||||
"merge-stream": "^2.0.0",
|
||||
"method-override": "^3.0.0",
|
||||
"moment": "^2.29.0",
|
||||
"moment": "^2.29.1",
|
||||
"moment-recur": "^1.0.7",
|
||||
"mongoose": "^5.10.3",
|
||||
"mongoose": "^5.10.11",
|
||||
"morgan": "^1.10.0",
|
||||
"nconf": "^0.10.0",
|
||||
"node-gcm": "^1.0.3",
|
||||
@@ -60,7 +60,7 @@
|
||||
"paypal-rest-sdk": "^1.8.1",
|
||||
"pp-ipn": "^1.1.0",
|
||||
"ps-tree": "^1.0.0",
|
||||
"rate-limiter-flexible": "^2.1.10",
|
||||
"rate-limiter-flexible": "^2.1.13",
|
||||
"redis": "^3.0.2",
|
||||
"regenerator-runtime": "^0.13.7",
|
||||
"remove-markdown": "^0.3.0",
|
||||
@@ -70,7 +70,7 @@
|
||||
"superagent": "^6.1.0",
|
||||
"universal-analytics": "^0.4.23",
|
||||
"useragent": "^2.1.9",
|
||||
"uuid": "^8.3.0",
|
||||
"uuid": "^8.3.1",
|
||||
"validator": "^13.1.17",
|
||||
"vinyl-buffer": "^1.0.1",
|
||||
"winston": "^3.3.3",
|
||||
@@ -109,7 +109,7 @@
|
||||
"apidoc": "gulp apidoc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"axios": "^0.19.2",
|
||||
"axios": "^0.21.0",
|
||||
"chai": "^4.1.2",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"chai-moment": "^0.1.0",
|
||||
@@ -121,7 +121,7 @@
|
||||
"monk": "^7.3.2",
|
||||
"require-again": "^2.0.0",
|
||||
"run-rs": "^0.6.2",
|
||||
"sinon": "^9.0.3",
|
||||
"sinon": "^9.2.1",
|
||||
"sinon-chai": "^3.5.0",
|
||||
"sinon-stub-promise": "^4.0.0"
|
||||
},
|
||||
|
||||
@@ -248,6 +248,7 @@ describe('payments/index', () => {
|
||||
quantity: 1,
|
||||
gift: true,
|
||||
purchaseValue: 15,
|
||||
firstPurchase: true,
|
||||
headers: {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
@@ -345,6 +346,7 @@ describe('payments/index', () => {
|
||||
quantity: 1,
|
||||
gift: false,
|
||||
purchaseValue: 15,
|
||||
firstPurchase: true,
|
||||
headers: {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
@@ -421,10 +423,22 @@ describe('payments/index', () => {
|
||||
});
|
||||
|
||||
context('Mystery Items', () => {
|
||||
it('awards mystery items when within the timeframe for a mystery item', async () => {
|
||||
const mayMysteryItemTimeframe = 1464725113000; // May 31st 2016
|
||||
const fakeClock = sinon.useFakeTimers(mayMysteryItemTimeframe);
|
||||
let clock;
|
||||
const mayMysteryItem = 'armor_mystery_201605';
|
||||
|
||||
beforeEach(() => {
|
||||
const mayMysteryItemTimeframe = new Date(2016, 4, 31); // May 31st 2016
|
||||
clock = sinon.useFakeTimers({
|
||||
now: mayMysteryItemTimeframe,
|
||||
toFake: ['Date'],
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (clock) clock.restore();
|
||||
});
|
||||
|
||||
it('awards mystery items when within the timeframe for a mystery item', async () => {
|
||||
data = { paymentMethod: 'PaymentMethod', user, sub: { key: 'basic_3mo' } };
|
||||
|
||||
const oldNotificationsCount = user.notifications.length;
|
||||
@@ -437,14 +451,9 @@ describe('payments/index', () => {
|
||||
expect(user.purchased.plan.mysteryItems).to.include('head_mystery_201605');
|
||||
expect(user.notifications.length).to.equal(oldNotificationsCount + 1);
|
||||
expect(user.notifications[0].type).to.equal('NEW_MYSTERY_ITEMS');
|
||||
|
||||
fakeClock.restore();
|
||||
});
|
||||
|
||||
it('does not award mystery item when user already owns the item', async () => {
|
||||
const mayMysteryItemTimeframe = 1464725113000; // May 31st 2016
|
||||
const fakeClock = sinon.useFakeTimers(mayMysteryItemTimeframe);
|
||||
const mayMysteryItem = 'armor_mystery_201605';
|
||||
user.items.gear.owned[mayMysteryItem] = true;
|
||||
|
||||
data = { paymentMethod: 'PaymentMethod', user, sub: { key: 'basic_3mo' } };
|
||||
@@ -453,14 +462,9 @@ describe('payments/index', () => {
|
||||
|
||||
expect(user.purchased.plan.mysteryItems).to.have.a.lengthOf(1);
|
||||
expect(user.purchased.plan.mysteryItems).to.include('head_mystery_201605');
|
||||
|
||||
fakeClock.restore();
|
||||
});
|
||||
|
||||
it('does not award mystery item when user already has the item in the mystery box', async () => {
|
||||
const mayMysteryItemTimeframe = 1464725113000; // May 31st 2016
|
||||
const fakeClock = sinon.useFakeTimers(mayMysteryItemTimeframe);
|
||||
const mayMysteryItem = 'armor_mystery_201605';
|
||||
user.purchased.plan.mysteryItems = [mayMysteryItem];
|
||||
|
||||
sandbox.spy(user.purchased.plan.mysteryItems, 'push');
|
||||
@@ -470,8 +474,6 @@ describe('payments/index', () => {
|
||||
|
||||
expect(user.purchased.plan.mysteryItems.push).to.be.calledOnce;
|
||||
expect(user.purchased.plan.mysteryItems.push).to.be.calledWith('head_mystery_201605');
|
||||
|
||||
fakeClock.restore();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -293,4 +293,90 @@ describe('cron middleware', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Drop Cap A/B Test', async () => {
|
||||
it('enrolls web users', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
await user.save();
|
||||
req.headers['x-client'] = 'habitica-web';
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, async err => {
|
||||
if (err) return reject(err);
|
||||
user = await User.findById(user._id).exec();
|
||||
expect(user._ABtests.dropCapNotif).to.be.a.string;
|
||||
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('enables the new notification for 50% of users', async () => {
|
||||
sandbox.stub(Math, 'random').returns(0.5);
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
await user.save();
|
||||
req.headers['x-client'] = 'habitica-web';
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, async err => {
|
||||
if (err) return reject(err);
|
||||
user = await User.findById(user._id).exec();
|
||||
expect(user._ABtests.dropCapNotif).to.be.equal('drop-cap-notif-enabled');
|
||||
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('disables the new notification for 50% of users', async () => {
|
||||
sandbox.stub(Math, 'random').returns(0.51);
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
await user.save();
|
||||
req.headers['x-client'] = 'habitica-web';
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, async err => {
|
||||
if (err) return reject(err);
|
||||
user = await User.findById(user._id).exec();
|
||||
expect(user._ABtests.dropCapNotif).to.be.equal('drop-cap-notif-disabled');
|
||||
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('does not affect subscribers', async () => {
|
||||
sandbox.stub(Math, 'random').returns(0.2);
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
await user.save();
|
||||
req.headers['x-client'] = 'habitica-web';
|
||||
sandbox.stub(User.prototype, 'isSubscribed').returns(true);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, async err => {
|
||||
if (err) return reject(err);
|
||||
user = await User.findById(user._id).exec();
|
||||
expect(user._ABtests.dropCapNotif).to.not.exist;
|
||||
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('does not affect mobile users', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
await user.save();
|
||||
req.headers['x-client'] = 'habitica-ios';
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, async err => {
|
||||
if (err) return reject(err);
|
||||
user = await User.findById(user._id).exec();
|
||||
expect(user._ABtests.dropCapNotif).to.not.exist;
|
||||
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
generateNext,
|
||||
} from '../../../helpers/api-unit.helper';
|
||||
import i18n from '../../../../website/common/script/i18n';
|
||||
import { ensureAdmin, ensureSudo } from '../../../../website/server/middlewares/ensureAccessRight';
|
||||
import { ensureAdmin, ensureSudo, ensureNewsPoster } from '../../../../website/server/middlewares/ensureAccessRight';
|
||||
import { NotAuthorized } from '../../../../website/server/libs/errors';
|
||||
import apiError from '../../../../website/server/libs/apiError';
|
||||
|
||||
@@ -40,6 +40,27 @@ describe('ensure access middlewares', () => {
|
||||
});
|
||||
});
|
||||
|
||||
context('ensure newsPoster', () => {
|
||||
it('returns not authorized when user is not a newsPoster', () => {
|
||||
res.locals = { user: { contributor: { newsPoster: false } } };
|
||||
|
||||
ensureNewsPoster(req, res, next);
|
||||
|
||||
const calledWith = next.getCall(0).args;
|
||||
expect(calledWith[0].message).to.equal(apiError('noNewsPosterAccess'));
|
||||
expect(calledWith[0] instanceof NotAuthorized).to.equal(true);
|
||||
});
|
||||
|
||||
it('passes when user is a newsPoster', () => {
|
||||
res.locals = { user: { contributor: { newsPoster: true } } };
|
||||
|
||||
ensureNewsPoster(req, res, next);
|
||||
|
||||
expect(next).to.be.calledOnce;
|
||||
expect(next.args[0]).to.be.empty;
|
||||
});
|
||||
});
|
||||
|
||||
context('ensure sudo', () => {
|
||||
it('returns not authorized when user is not a sudo user', () => {
|
||||
res.locals = { user: { contributor: { sudo: false } } };
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import { v4 } from 'uuid';
|
||||
import { model as NewsPost, refreshNewsPost } from '../../../../website/server/models/newsPost';
|
||||
import { sleep } from '../../../helpers/api-unit.helper';
|
||||
|
||||
describe('NewsPost Model', () => {
|
||||
const publishDate = Number(new Date());
|
||||
|
||||
// NOTE publishDate is manually increased by +500 for each test
|
||||
// to make sure it's always in the future from the previous one
|
||||
// bevause NewsPost.lastNewsPost() is not reset between tests.
|
||||
// And without a more recent publishDate it wouldn't update
|
||||
|
||||
it('#lastNewsPost', () => {
|
||||
const lastPost = { _id: v4(), publishDate, published: true };
|
||||
NewsPost.updateLastNewsPost(lastPost);
|
||||
expect(NewsPost.lastNewsPost()).to.equal(lastPost);
|
||||
});
|
||||
|
||||
it('#getLastPostFromDatabase', async () => {
|
||||
const expectedId = v4();
|
||||
|
||||
await NewsPost.create([
|
||||
// more recent but not published
|
||||
{
|
||||
_id: v4(),
|
||||
publishDate: new Date(publishDate + 50),
|
||||
author: v4(),
|
||||
published: false,
|
||||
title: 'Title',
|
||||
credits: 'credits',
|
||||
text: 'text',
|
||||
},
|
||||
// expected
|
||||
{
|
||||
_id: expectedId,
|
||||
publishDate,
|
||||
author: v4(),
|
||||
published: true,
|
||||
title: 'Title',
|
||||
credits: 'credits',
|
||||
text: 'text',
|
||||
},
|
||||
// published but less recent
|
||||
{
|
||||
_id: v4(),
|
||||
publishDate: new Date(Number(publishDate) - 50),
|
||||
author: v4(),
|
||||
published: true,
|
||||
title: 'Title',
|
||||
credits: 'credits',
|
||||
text: 'text',
|
||||
},
|
||||
]);
|
||||
|
||||
const fetched = await NewsPost.getLastPostFromDatabase();
|
||||
expect(fetched._id).to.equal(expectedId);
|
||||
});
|
||||
|
||||
context('#updateLastNewsPost', () => {
|
||||
it('updates the post if new one is more recent and published', () => {
|
||||
const previousPost = {
|
||||
_id: v4(),
|
||||
publishDate: new Date(publishDate + 100),
|
||||
published: true,
|
||||
};
|
||||
NewsPost.updateLastNewsPost(previousPost);
|
||||
const newPost = {
|
||||
_id: v4(),
|
||||
publishDate: new Date(publishDate + 150),
|
||||
published: true,
|
||||
};
|
||||
NewsPost.updateLastNewsPost(newPost);
|
||||
expect(NewsPost.lastNewsPost()._id).to.equal(newPost._id);
|
||||
});
|
||||
|
||||
it('does not update the post if new one is from the past', () => {
|
||||
const previousPost = new NewsPost({
|
||||
_id: v4(), publishDate: new Date(publishDate + 200), published: true,
|
||||
});
|
||||
NewsPost.updateLastNewsPost(previousPost);
|
||||
const newPost = new NewsPost({
|
||||
_id: v4(), publishDate: new Date(publishDate + 175), published: true,
|
||||
});
|
||||
NewsPost.updateLastNewsPost(newPost);
|
||||
expect(NewsPost.lastNewsPost()._id).to.equal(previousPost._id);
|
||||
});
|
||||
|
||||
it('does not update the post if new one is not published', () => {
|
||||
const previousPost = new NewsPost({
|
||||
_id: v4(), publishDate: new Date(publishDate + 250), published: true,
|
||||
});
|
||||
NewsPost.updateLastNewsPost(previousPost);
|
||||
const newPost = new NewsPost({
|
||||
_id: v4(), publishDate: new Date(publishDate + 300), published: false,
|
||||
});
|
||||
NewsPost.updateLastNewsPost(newPost);
|
||||
expect(NewsPost.lastNewsPost()._id).to.equal(previousPost._id);
|
||||
});
|
||||
});
|
||||
|
||||
context('refreshes NewsPost', () => {
|
||||
let intervalId;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Delete all existing posts from the database
|
||||
await NewsPost.remove();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (intervalId) clearInterval(intervalId);
|
||||
});
|
||||
|
||||
it('refreshes the last post at a specific interval', async () => {
|
||||
await sleep(0.1); // wait 100ms to make sure all previous posts are in the past
|
||||
const previousPost = {
|
||||
_id: v4(), publishDate: new Date(), published: true,
|
||||
};
|
||||
NewsPost.updateLastNewsPost(previousPost);
|
||||
intervalId = refreshNewsPost(50); // refreshes every 50ms
|
||||
|
||||
await sleep(0.1); // wait 100ms to make sure the new post has a more recent publishDate
|
||||
const newPost = await NewsPost.create({
|
||||
_id: v4(),
|
||||
publishDate: new Date(),
|
||||
author: v4(),
|
||||
published: true,
|
||||
title: 'Title',
|
||||
credits: 'credits',
|
||||
text: 'text',
|
||||
});
|
||||
|
||||
expect(NewsPost.lastNewsPost()._id).to.equal(previousPost._id);
|
||||
await sleep(0.15); // wait 150ms
|
||||
|
||||
expect(NewsPost.lastNewsPost()._id).to.equal(newPost._id);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,87 +1,90 @@
|
||||
import moment from 'moment';
|
||||
import { model as User } from '../../../../website/server/models/user';
|
||||
import { model as NewsPost } from '../../../../website/server/models/newsPost';
|
||||
import { model as Group } from '../../../../website/server/models/group';
|
||||
import common from '../../../../website/common';
|
||||
|
||||
describe('User Model', () => {
|
||||
it('keeps user._tmp when calling .toJSON', () => {
|
||||
const user = new User({
|
||||
auth: {
|
||||
local: {
|
||||
username: 'username',
|
||||
lowerCaseUsername: 'username',
|
||||
email: 'email@email.email',
|
||||
salt: 'salt',
|
||||
hashed_password: 'hashed_password', // eslint-disable-line camelcase
|
||||
describe('.toJSON()', () => {
|
||||
it('keeps user._tmp when calling .toJSON', () => {
|
||||
const user = new User({
|
||||
auth: {
|
||||
local: {
|
||||
username: 'username',
|
||||
lowerCaseUsername: 'username',
|
||||
email: 'email@email.email',
|
||||
salt: 'salt',
|
||||
hashed_password: 'hashed_password', // eslint-disable-line camelcase
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
user._tmp = { ok: true };
|
||||
user._nonTmp = { ok: true };
|
||||
|
||||
expect(user._tmp).to.eql({ ok: true });
|
||||
expect(user._nonTmp).to.eql({ ok: true });
|
||||
|
||||
const toObject = user.toObject();
|
||||
const toJSON = user.toJSON();
|
||||
|
||||
expect(toObject).to.not.have.keys('_tmp');
|
||||
expect(toObject).to.not.have.keys('_nonTmp');
|
||||
|
||||
expect(toJSON).to.have.any.key('_tmp');
|
||||
expect(toJSON._tmp).to.eql({ ok: true });
|
||||
expect(toJSON).to.not.have.keys('_nonTmp');
|
||||
});
|
||||
|
||||
user._tmp = { ok: true };
|
||||
user._nonTmp = { ok: true };
|
||||
it('can add computed stats to a JSONified user object', () => {
|
||||
const user = new User();
|
||||
const userToJSON = user.toJSON();
|
||||
|
||||
expect(user._tmp).to.eql({ ok: true });
|
||||
expect(user._nonTmp).to.eql({ ok: true });
|
||||
expect(userToJSON.stats.maxMP).to.not.exist;
|
||||
expect(userToJSON.stats.maxHealth).to.not.exist;
|
||||
expect(userToJSON.stats.toNextLevel).to.not.exist;
|
||||
|
||||
const toObject = user.toObject();
|
||||
const toJSON = user.toJSON();
|
||||
User.addComputedStatsToJSONObj(userToJSON.stats, userToJSON);
|
||||
|
||||
expect(toObject).to.not.have.keys('_tmp');
|
||||
expect(toObject).to.not.have.keys('_nonTmp');
|
||||
expect(userToJSON.stats.maxMP).to.exist;
|
||||
expect(userToJSON.stats.maxHealth).to.equal(common.maxHealth);
|
||||
expect(userToJSON.stats.toNextLevel).to.equal(common.tnl(user.stats.lvl));
|
||||
});
|
||||
|
||||
expect(toJSON).to.have.any.key('_tmp');
|
||||
expect(toJSON._tmp).to.eql({ ok: true });
|
||||
expect(toJSON).to.not.have.keys('_nonTmp');
|
||||
});
|
||||
it('can transform user object without mongoose helpers', async () => {
|
||||
const user = new User();
|
||||
await user.save();
|
||||
const userToJSON = await User.findById(user._id).lean().exec();
|
||||
|
||||
it('can add computed stats to a JSONified user object', () => {
|
||||
const user = new User();
|
||||
const userToJSON = user.toJSON();
|
||||
expect(userToJSON.stats.maxMP).to.not.exist;
|
||||
expect(userToJSON.stats.maxHealth).to.not.exist;
|
||||
expect(userToJSON.stats.toNextLevel).to.not.exist;
|
||||
expect(userToJSON.id).to.not.exist;
|
||||
|
||||
expect(userToJSON.stats.maxMP).to.not.exist;
|
||||
expect(userToJSON.stats.maxHealth).to.not.exist;
|
||||
expect(userToJSON.stats.toNextLevel).to.not.exist;
|
||||
User.transformJSONUser(userToJSON);
|
||||
|
||||
User.addComputedStatsToJSONObj(userToJSON.stats, userToJSON);
|
||||
expect(userToJSON.id).to.equal(userToJSON._id);
|
||||
expect(userToJSON.stats.maxMP).to.not.exist;
|
||||
expect(userToJSON.stats.maxHealth).to.not.exist;
|
||||
expect(userToJSON.stats.toNextLevel).to.not.exist;
|
||||
});
|
||||
|
||||
expect(userToJSON.stats.maxMP).to.exist;
|
||||
expect(userToJSON.stats.maxHealth).to.equal(common.maxHealth);
|
||||
expect(userToJSON.stats.toNextLevel).to.equal(common.tnl(user.stats.lvl));
|
||||
});
|
||||
it('can transform user object without mongoose helpers (including computed stats)', async () => {
|
||||
const user = new User();
|
||||
await user.save();
|
||||
const userToJSON = await User.findById(user._id).lean().exec();
|
||||
|
||||
it('can transform user object without mongoose helpers', async () => {
|
||||
const user = new User();
|
||||
await user.save();
|
||||
const userToJSON = await User.findById(user._id).lean().exec();
|
||||
expect(userToJSON.stats.maxMP).to.not.exist;
|
||||
expect(userToJSON.stats.maxHealth).to.not.exist;
|
||||
expect(userToJSON.stats.toNextLevel).to.not.exist;
|
||||
|
||||
expect(userToJSON.stats.maxMP).to.not.exist;
|
||||
expect(userToJSON.stats.maxHealth).to.not.exist;
|
||||
expect(userToJSON.stats.toNextLevel).to.not.exist;
|
||||
expect(userToJSON.id).to.not.exist;
|
||||
User.transformJSONUser(userToJSON, true);
|
||||
|
||||
User.transformJSONUser(userToJSON);
|
||||
|
||||
expect(userToJSON.id).to.equal(userToJSON._id);
|
||||
expect(userToJSON.stats.maxMP).to.not.exist;
|
||||
expect(userToJSON.stats.maxHealth).to.not.exist;
|
||||
expect(userToJSON.stats.toNextLevel).to.not.exist;
|
||||
});
|
||||
|
||||
it('can transform user object without mongoose helpers (including computed stats)', async () => {
|
||||
const user = new User();
|
||||
await user.save();
|
||||
const userToJSON = await User.findById(user._id).lean().exec();
|
||||
|
||||
expect(userToJSON.stats.maxMP).to.not.exist;
|
||||
expect(userToJSON.stats.maxHealth).to.not.exist;
|
||||
expect(userToJSON.stats.toNextLevel).to.not.exist;
|
||||
|
||||
User.transformJSONUser(userToJSON, true);
|
||||
|
||||
expect(userToJSON.id).to.equal(userToJSON._id);
|
||||
expect(userToJSON.stats.maxMP).to.exist;
|
||||
expect(userToJSON.stats.maxHealth).to.equal(common.maxHealth);
|
||||
expect(userToJSON.stats.toNextLevel).to.equal(common.tnl(user.stats.lvl));
|
||||
expect(userToJSON.id).to.equal(userToJSON._id);
|
||||
expect(userToJSON.stats.maxMP).to.exist;
|
||||
expect(userToJSON.stats.maxHealth).to.equal(common.maxHealth);
|
||||
expect(userToJSON.stats.toNextLevel).to.equal(common.tnl(user.stats.lvl));
|
||||
});
|
||||
});
|
||||
|
||||
context('achievements', () => {
|
||||
@@ -589,6 +592,50 @@ describe('User Model', () => {
|
||||
});
|
||||
|
||||
context('pre-save hook', () => {
|
||||
it('enrolls users that signup through web in the Drop Cap AB test', async () => {
|
||||
let user = new User();
|
||||
user.registeredThrough = 'habitica-web';
|
||||
user = await user.save();
|
||||
expect(user._ABtests.dropCapNotif).to.exist;
|
||||
});
|
||||
|
||||
it('does not enroll users that signup through modal in the Drop Cap AB test', async () => {
|
||||
let user = new User();
|
||||
user.registeredThrough = 'habitica-ios';
|
||||
user = await user.save();
|
||||
expect(user._ABtests.dropCapNotif).to.not.exist;
|
||||
});
|
||||
|
||||
it('marks the last news post as read for new users', async () => {
|
||||
const lastNewsPost = { _id: '1' };
|
||||
sandbox.stub(NewsPost, 'lastNewsPost').returns(lastNewsPost);
|
||||
|
||||
let user = new User();
|
||||
expect(user.isNew).to.equal(true);
|
||||
user = await user.save();
|
||||
|
||||
expect(user.checkNewStuff()).to.equal(false);
|
||||
expect(user.toJSON().flags.newStuff).to.equal(false);
|
||||
expect(user.flags.lastNewStuffRead).to.equal(lastNewsPost._id);
|
||||
});
|
||||
|
||||
it('does not mark the last news post as read for existing users', async () => {
|
||||
const lastNewsPost = { _id: '1' };
|
||||
const lastNewsPostStub = sandbox.stub(NewsPost, 'lastNewsPost');
|
||||
lastNewsPostStub.returns(lastNewsPost);
|
||||
|
||||
let user = new User();
|
||||
user = await user.save();
|
||||
|
||||
expect(user.isNew).to.equal(false);
|
||||
user.profile.name = 'new name';
|
||||
|
||||
lastNewsPostStub.returns({ _id: '2' });
|
||||
user = await user.save();
|
||||
|
||||
expect(user.flags.lastNewStuffRead).to.equal(lastNewsPost._id); // not _id: 2
|
||||
});
|
||||
|
||||
it('does not try to award achievements when achievements or items not selected in query', async () => {
|
||||
let user = new User();
|
||||
user = await user.save(); // necessary for user.isSelected to work correctly
|
||||
@@ -827,4 +874,46 @@ describe('User Model', () => {
|
||||
expect(daysMissed).to.eql(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('isNewsPoster', async () => {
|
||||
const user = new User();
|
||||
await user.save();
|
||||
|
||||
expect(user.isNewsPoster()).to.equal(false);
|
||||
|
||||
user.contributor.newsPoster = true;
|
||||
expect(user.isNewsPoster()).to.equal(true);
|
||||
});
|
||||
|
||||
describe('checkNewStuff', () => {
|
||||
let user;
|
||||
|
||||
beforeEach(() => {
|
||||
user = new User();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('no last news post', () => {
|
||||
sandbox.stub(NewsPost, 'lastNewsPost').returns(null);
|
||||
expect(user.checkNewStuff()).to.equal(false);
|
||||
expect(user.toJSON().flags.newStuff).to.equal(false);
|
||||
});
|
||||
|
||||
it('last news post read', () => {
|
||||
sandbox.stub(NewsPost, 'lastNewsPost').returns({ _id: '123' });
|
||||
user.flags.lastNewStuffRead = '123';
|
||||
expect(user.checkNewStuff()).to.equal(false);
|
||||
expect(user.toJSON().flags.newStuff).to.equal(false);
|
||||
});
|
||||
|
||||
it('last news post not read', () => {
|
||||
sandbox.stub(NewsPost, 'lastNewsPost').returns({ _id: '123' });
|
||||
user.flags.lastNewStuffRead = '124';
|
||||
expect(user.checkNewStuff()).to.equal(true);
|
||||
expect(user.toJSON().flags.newStuff).to.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -251,6 +251,29 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
|
||||
expect(party.quest.members[partyMember._id]).to.not.exist;
|
||||
});
|
||||
|
||||
it('prevents user from being removed if they are the quest owner', async () => {
|
||||
const petQuest = 'whale';
|
||||
await partyMember.update({
|
||||
[`items.quests.${petQuest}`]: 1,
|
||||
});
|
||||
|
||||
await partyMember.post(`/groups/${party._id}/quests/invite/${petQuest}`);
|
||||
await partyLeader.post(`/groups/${party._id}/quests/accept`);
|
||||
|
||||
await party.sync();
|
||||
|
||||
expect(party.quest.members[partyLeader._id]).to.be.true;
|
||||
expect(party.quest.members[partyMember._id]).to.be.true;
|
||||
|
||||
await party.sync();
|
||||
|
||||
expect(leader.post(`/groups/${party._id}/removeMember/${partyMember._id}`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
text: t('cannotRemoveQuestOwner'),
|
||||
});
|
||||
});
|
||||
|
||||
it('sends email to user with rescinded invite', async () => {
|
||||
await partyLeader.post(`/groups/${party._id}/removeMember/${partyInvitedUser._id}`);
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
|
||||
describe('GET /news', () => {
|
||||
let api;
|
||||
|
||||
beforeEach(async () => {
|
||||
api = requester();
|
||||
});
|
||||
|
||||
@@ -1,24 +1,27 @@
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import { model as NewsPost } from '../../../../../website/server/models/newsPost';
|
||||
|
||||
describe('POST /news/tell-me-later', () => {
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser({
|
||||
'flags.newStuff': true,
|
||||
NewsPost.updateLastNewsPost({
|
||||
_id: '1234', publishDate: new Date(), title: 'Title', published: true,
|
||||
});
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('marks new stuff as read and adds notification', async () => {
|
||||
expect(user.flags.newStuff).to.equal(true);
|
||||
const initialNotifications = user.notifications.length;
|
||||
|
||||
await user.post('/news/tell-me-later');
|
||||
await user.sync();
|
||||
|
||||
expect(user.flags.newStuff).to.equal(false);
|
||||
expect(user.flags.lastNewStuffRead).to.equal('1234');
|
||||
// fetching the user because newStuff is a computed property
|
||||
expect((await user.get('/user')).flags.newStuff).to.equal(false);
|
||||
expect(user.notifications.length).to.equal(initialNotifications + 1);
|
||||
|
||||
const notification = user.notifications[user.notifications.length - 1];
|
||||
|
||||
@@ -83,22 +83,6 @@ describe('POST /groups/:groupId/quests/invite/:questKey', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('does not issue invites if the user is of insufficient Level', async () => {
|
||||
const LEVELED_QUEST = 'atom1';
|
||||
const LEVELED_QUEST_REQ = questScrolls[LEVELED_QUEST].lvl;
|
||||
const leaderUpdate = {};
|
||||
leaderUpdate[`items.quests.${LEVELED_QUEST}`] = 1;
|
||||
leaderUpdate['stats.lvl'] = LEVELED_QUEST_REQ - 1;
|
||||
|
||||
await leader.update(leaderUpdate);
|
||||
|
||||
await expect(leader.post(`/groups/${questingGroup._id}/quests/invite/${LEVELED_QUEST}`)).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('questLevelTooHigh', { level: LEVELED_QUEST_REQ }),
|
||||
});
|
||||
});
|
||||
|
||||
it('does not issue invites if a quest is already underway', async () => {
|
||||
const QUEST_IN_PROGRESS = 'atom1';
|
||||
const leaderUpdate = {};
|
||||
@@ -212,6 +196,18 @@ describe('POST /groups/:groupId/quests/invite/:questKey', () => {
|
||||
expect(returnedGroup.chat[0]._meta).to.be.undefined;
|
||||
});
|
||||
|
||||
it('successfully issues a quest invitation when quest level is higher than user level', async () => {
|
||||
const LEVELED_QUEST = 'atom1';
|
||||
const LEVELED_QUEST_REQ = questScrolls[LEVELED_QUEST].lvl;
|
||||
const leaderUpdate = {};
|
||||
leaderUpdate[`items.quests.${LEVELED_QUEST}`] = 1;
|
||||
leaderUpdate['stats.lvl'] = LEVELED_QUEST_REQ - 1;
|
||||
|
||||
await leader.update(leaderUpdate);
|
||||
|
||||
await leader.post(`/groups/${questingGroup._id}/quests/invite/${LEVELED_QUEST}`);
|
||||
});
|
||||
|
||||
context('sending quest activity webhooks', () => {
|
||||
before(async () => {
|
||||
await server.start();
|
||||
|
||||
@@ -91,7 +91,9 @@ describe('POST /tasks/:taskId/move/to/:position', () => {
|
||||
|
||||
const taskToMove = tasks[1];
|
||||
expect(taskToMove.text).to.equal('habit 2');
|
||||
const newOrder = await user.post(`/tasks/${tasks[1]._id}/move/to/-1`);
|
||||
await user.post(`/tasks/${tasks[1]._id}/move/to/-1`);
|
||||
await user.sync();
|
||||
const newOrder = user.tasksOrder.habits;
|
||||
expect(newOrder[4]).to.equal(taskToMove._id);
|
||||
expect(newOrder.length).to.equal(5);
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import { model as NewsPost } from '../../../../../website/server/models/newsPost';
|
||||
|
||||
describe('PUT /user', () => {
|
||||
let user;
|
||||
@@ -101,6 +102,24 @@ describe('PUT /user', () => {
|
||||
message: t('displaynameIssueNewline'),
|
||||
});
|
||||
});
|
||||
|
||||
it('can set flags.newStuff to false', async () => {
|
||||
NewsPost.updateLastNewsPost({
|
||||
_id: '1234', publishDate: new Date(), title: 'Title', published: true,
|
||||
});
|
||||
|
||||
await user.update({
|
||||
'flags.lastNewStuffRead': '123',
|
||||
});
|
||||
|
||||
await user.put('/user', {
|
||||
'flags.newStuff': false,
|
||||
});
|
||||
|
||||
await user.sync();
|
||||
|
||||
expect(user.flags.lastNewStuffRead).to.eql('1234');
|
||||
});
|
||||
});
|
||||
|
||||
context('Top Level Protected Operations', () => {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { v4 } from 'uuid';
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../helpers/api-integration/v4';
|
||||
|
||||
describe('DELETE /news/:newsID', () => {
|
||||
let user;
|
||||
const newsPost = {
|
||||
title: 'New Post',
|
||||
publishDate: new Date(),
|
||||
published: true,
|
||||
credits: 'credits',
|
||||
text: 'news body',
|
||||
};
|
||||
beforeEach(async () => {
|
||||
user = await generateUser({
|
||||
'contributor.newsPoster': true,
|
||||
});
|
||||
});
|
||||
|
||||
it('disallows access to non-newsPosters', async () => {
|
||||
const nonAdminUser = await generateUser({ 'contributor.newsPoster': false });
|
||||
await expect(nonAdminUser.del(`/news/${v4()}`)).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: 'You don\'t have news poster access.',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if the post does not exist', async () => {
|
||||
await expect(user.del(`/news/${v4()}`)).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('newsPostNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('deletes news posts', async () => {
|
||||
const existingPost = await user.post('/news', newsPost);
|
||||
await user.del(`/news/${existingPost._id}`);
|
||||
|
||||
const returnedPosts = await user.get('/news');
|
||||
const deletedPost = returnedPosts.find(returnedPost => returnedPost._id === existingPost._id);
|
||||
|
||||
expect(returnedPosts).is.an('array');
|
||||
expect(deletedPost).to.not.exist;
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
import {
|
||||
requester, generateUser,
|
||||
} from '../../../helpers/api-integration/v4';
|
||||
|
||||
describe('GET /news', () => {
|
||||
let api;
|
||||
const newsPost = {
|
||||
title: 'New Post',
|
||||
publishDate: new Date(),
|
||||
published: true,
|
||||
credits: 'credits',
|
||||
text: 'news body',
|
||||
};
|
||||
|
||||
before(async () => {
|
||||
api = requester();
|
||||
const user = await generateUser({
|
||||
'contributor.newsPoster': true,
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
user.post('/news', newsPost),
|
||||
user.post('/news', newsPost),
|
||||
user.post('/news', newsPost),
|
||||
user.post('/news', newsPost),
|
||||
user.post('/news', newsPost),
|
||||
user.post('/news', newsPost),
|
||||
user.post('/news', newsPost),
|
||||
user.post('/news', newsPost),
|
||||
user.post('/news', newsPost),
|
||||
user.post('/news', newsPost),
|
||||
user.post('/news', newsPost),
|
||||
user.post('/news', newsPost),
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns the latest news in json format, does not require authentication, 10 per page', async () => {
|
||||
const res = await api.get('/news');
|
||||
expect(res.length).to.be.equal(10);
|
||||
expect(res[0].title).to.be.not.empty;
|
||||
expect(res[0].text).to.be.not.empty;
|
||||
});
|
||||
|
||||
it('supports pagination', async () => {
|
||||
const res = await api.get('/news?page=1');
|
||||
expect(res.length).to.be.equal(2);
|
||||
expect(res[0].title).to.be.not.empty;
|
||||
expect(res[0].text).to.be.not.empty;
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { v4 } from 'uuid';
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../helpers/api-integration/v4';
|
||||
|
||||
describe('GET /news/:newsID', () => {
|
||||
let user;
|
||||
const newsPost = {
|
||||
title: 'New Post',
|
||||
publishDate: new Date(),
|
||||
published: true,
|
||||
credits: 'credits',
|
||||
text: 'news body',
|
||||
};
|
||||
beforeEach(async () => {
|
||||
user = await generateUser({
|
||||
'contributor.newsPoster': true,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if the post does not exist', async () => {
|
||||
await expect(user.get(`/news/${v4()}`)).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('newsPostNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('fetches an existing post', async () => {
|
||||
const existingPost = await user.post('/news', newsPost);
|
||||
const fetchedPost = await user.get(`/news/${existingPost._id}`);
|
||||
|
||||
expect(fetchedPost._id).to.equal(existingPost._id);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,134 @@
|
||||
import moment from 'moment';
|
||||
import {
|
||||
generateUser,
|
||||
sleep,
|
||||
} from '../../../helpers/api-integration/v4';
|
||||
import { model as NewsPost } from '../../../../website/server/models/newsPost';
|
||||
|
||||
describe('POST /news', () => {
|
||||
let user;
|
||||
const newsPost = {
|
||||
title: 'New Post',
|
||||
publishDate: new Date(),
|
||||
published: true,
|
||||
credits: 'credits',
|
||||
text: 'news body',
|
||||
};
|
||||
beforeEach(async () => {
|
||||
user = await generateUser({
|
||||
'contributor.newsPoster': true,
|
||||
});
|
||||
});
|
||||
|
||||
it('disallows access to non-admins', async () => {
|
||||
const nonAdminUser = await generateUser({ 'contributor.newsPoster': false });
|
||||
await expect(nonAdminUser.post('/news')).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: 'You don\'t have news poster access.',
|
||||
});
|
||||
});
|
||||
|
||||
it('creates news posts', async () => {
|
||||
const response = await user.post('/news', newsPost);
|
||||
|
||||
expect(response.title).to.equal(newsPost.title);
|
||||
expect(response.credits).to.equal(newsPost.credits);
|
||||
expect(response.text).to.equal(newsPost.text);
|
||||
expect(response._id).to.exist;
|
||||
|
||||
const res = await user.get('/news');
|
||||
expect(res[0]._id).to.equal(response._id);
|
||||
expect(res[0].title).to.equal(newsPost.title);
|
||||
expect(res[0].text).to.equal(newsPost.text);
|
||||
});
|
||||
|
||||
context('calls updateLastNewsPost', () => {
|
||||
beforeEach(async () => {
|
||||
await NewsPost.remove({ });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
newsPost.publishDate = new Date();
|
||||
newsPost.published = true;
|
||||
});
|
||||
|
||||
it('new post is published and the most recent one', async () => {
|
||||
newsPost.publishDate = new Date();
|
||||
const newPost = await user.post('/news', newsPost);
|
||||
await sleep(0.05);
|
||||
expect(NewsPost.lastNewsPost()._id).to.equal(newPost._id);
|
||||
});
|
||||
|
||||
it('new post is not published', async () => {
|
||||
newsPost.published = false;
|
||||
const newPost = await user.post('/news', newsPost);
|
||||
await sleep(0.05);
|
||||
expect(NewsPost.lastNewsPost()._id).to.not.equal(newPost._id);
|
||||
});
|
||||
|
||||
it('new post is published but in the future', async () => {
|
||||
newsPost.publishDate = moment().add({ days: 1 }).toDate();
|
||||
const newPost = await user.post('/news', newsPost);
|
||||
await sleep(0.05);
|
||||
expect(NewsPost.lastNewsPost()._id).to.not.equal(newPost._id);
|
||||
});
|
||||
|
||||
it('new post is published but not the most recent one', async () => {
|
||||
const oldPost = await user.post('/news', newsPost);
|
||||
newsPost.publishDate = moment().subtract({ days: 1 }).toDate();
|
||||
await user.post('/news', newsPost);
|
||||
await sleep(0.05);
|
||||
expect(NewsPost.lastNewsPost()._id).to.equal(oldPost._id);
|
||||
});
|
||||
});
|
||||
|
||||
it('sets default fields', async () => {
|
||||
const response = await user.post('/news', {
|
||||
title: 'A post',
|
||||
credits: 'Credits',
|
||||
text: 'Text',
|
||||
});
|
||||
|
||||
expect(response.published).to.equal(false);
|
||||
expect(response.publishDate).to.exist;
|
||||
expect(response.author).to.equal(user._id);
|
||||
expect(response.createdAt).to.exist;
|
||||
expect(response.updatedAt).to.exist;
|
||||
});
|
||||
|
||||
context('required fields', () => {
|
||||
it('title', async () => {
|
||||
await expect(user.post('/news', {
|
||||
text: 'Text',
|
||||
credits: 'Credits',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'NewsPost validation failed',
|
||||
});
|
||||
});
|
||||
|
||||
it('credits', async () => {
|
||||
await expect(user.post('/news', {
|
||||
text: 'Text',
|
||||
title: 'Title',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'NewsPost validation failed',
|
||||
});
|
||||
});
|
||||
|
||||
it('text', async () => {
|
||||
await expect(user.post('/news', {
|
||||
credits: 'credits',
|
||||
title: 'Title',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'NewsPost validation failed',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../helpers/api-integration/v4';
|
||||
import { model as NewsPost } from '../../../../website/server/models/newsPost';
|
||||
|
||||
describe('POST /news/read', () => {
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('marks new stuff as read', async () => {
|
||||
NewsPost.updateLastNewsPost({ _id: '1234', publishDate: new Date(), published: true });
|
||||
await user.post('/news/read');
|
||||
await user.sync();
|
||||
|
||||
expect(user.flags.lastNewStuffRead).to.equal('1234');
|
||||
// fetching the user because newStuff is a computed property
|
||||
expect((await user.get('/user')).flags.newStuff).to.equal(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,103 @@
|
||||
import { v4 } from 'uuid';
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
sleep,
|
||||
} from '../../../helpers/api-integration/v4';
|
||||
import { model as NewsPost } from '../../../../website/server/models/newsPost';
|
||||
|
||||
describe('PUT /news/:newsID', () => {
|
||||
let user;
|
||||
const newsPost = {
|
||||
title: 'New Post',
|
||||
publishDate: new Date(),
|
||||
published: true,
|
||||
credits: 'credits',
|
||||
text: 'news body',
|
||||
};
|
||||
beforeEach(async () => {
|
||||
user = await generateUser({
|
||||
'contributor.newsPoster': true,
|
||||
});
|
||||
});
|
||||
|
||||
it('disallows access to non-admins', async () => {
|
||||
const nonAdminUser = await generateUser({ 'contributor.newsPoster': false });
|
||||
await expect(nonAdminUser.put('/news/1234')).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: 'You don\'t have news poster access.',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if the post does not exist', async () => {
|
||||
await expect(user.put(`/news/${v4()}`)).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('newsPostNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('updates existing news posts', async () => {
|
||||
const existingPost = await user.post('/news', newsPost);
|
||||
const updatedPost = await user.put(`/news/${existingPost._id}`, {
|
||||
title: 'Changed Title',
|
||||
});
|
||||
|
||||
expect(updatedPost.title).to.equal('Changed Title');
|
||||
expect(updatedPost.credits).to.equal(existingPost.credits);
|
||||
expect(updatedPost.text).to.equal(existingPost.text);
|
||||
expect(updatedPost.published).to.equal(existingPost.published);
|
||||
expect(updatedPost._id).to.equal(existingPost._id);
|
||||
});
|
||||
|
||||
context('calls updateLastNewsPost', () => {
|
||||
beforeEach(async () => {
|
||||
await NewsPost.remove({ });
|
||||
});
|
||||
|
||||
it('updates post data', async () => {
|
||||
const existingPost = await user.post('/news', { ...newsPost, publishDate: new Date() });
|
||||
const updatedPost = await user.put(`/news/${existingPost._id}`, {
|
||||
title: 'Changed Title',
|
||||
});
|
||||
await sleep(0.05);
|
||||
|
||||
expect(NewsPost.lastNewsPost().title).to.equal(updatedPost.title);
|
||||
});
|
||||
|
||||
it('updated post is not published', async () => {
|
||||
const oldPost = await user.post('/news', { ...newsPost, publishDate: new Date() });
|
||||
const newUnpublished = await user.post('/news', { ...newsPost, published: false });
|
||||
await user.put(`/news/${newUnpublished._id}`, {
|
||||
title: 'Changed Title',
|
||||
});
|
||||
await sleep(0.05);
|
||||
|
||||
expect(NewsPost.lastNewsPost()._id).to.equal(oldPost._id);
|
||||
});
|
||||
|
||||
it('updated post is published', async () => {
|
||||
await user.post('/news', { ...newsPost, publishDate: new Date() });
|
||||
const newUnpublished = await user.post('/news', { ...newsPost, published: false, publishDate: new Date() });
|
||||
await user.put(`/news/${newUnpublished._id}`, {
|
||||
publishDate: new Date(),
|
||||
published: true,
|
||||
});
|
||||
await sleep(0.05);
|
||||
|
||||
expect(NewsPost.lastNewsPost()._id).to.equal(newUnpublished._id);
|
||||
});
|
||||
|
||||
it('updated post publishDate is in future', async () => {
|
||||
const oldPost = await user.post('/news', { ...newsPost, publishDate: new Date() });
|
||||
const newUnpublished = await user.post('/news', newsPost);
|
||||
await user.put(`/news/${newUnpublished._id}`, {
|
||||
publishDate: Date.now() + 50000,
|
||||
});
|
||||
await sleep(0.05);
|
||||
|
||||
expect(NewsPost.lastNewsPost()._id).to.equal(oldPost._id);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../helpers/api-integration/v4';
|
||||
import { UNEQUIP_EQUIPPED } from '../../../../website/common/script/ops/unequip';
|
||||
|
||||
describe('POST /user/unequip', () => {
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser({
|
||||
preferences: {
|
||||
background: 'violet',
|
||||
},
|
||||
items: {
|
||||
currentMount: 'BearCub-Base',
|
||||
currentPet: 'BearCub-Base',
|
||||
gear: {
|
||||
owned: {
|
||||
weapon_warrior_0: true,
|
||||
weapon_warrior_1: true,
|
||||
weapon_warrior_2: true,
|
||||
weapon_wizard_1: true,
|
||||
weapon_wizard_2: true,
|
||||
shield_base_0: true,
|
||||
shield_warrior_1: true,
|
||||
},
|
||||
equipped: {
|
||||
weapon: 'weapon_warrior_2',
|
||||
shield: 'shield_warrior_1',
|
||||
},
|
||||
costume: {
|
||||
weapon: 'weapon_warrior_2',
|
||||
shield: 'shield_warrior_1',
|
||||
},
|
||||
},
|
||||
},
|
||||
stats: { gp: 200 },
|
||||
});
|
||||
});
|
||||
|
||||
// More tests in common code unit tests
|
||||
|
||||
context('Gear', () => {
|
||||
it('should unequip all battle gear items', async () => {
|
||||
await user.post(`/user/unequip/${UNEQUIP_EQUIPPED}`);
|
||||
await user.sync();
|
||||
|
||||
expect(user.items.gear.equipped.weapon).to.eq('weapon_base_0');
|
||||
expect(user.items.gear.equipped.shield).to.eq('shield_base_0');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import randomDrop from '../../../website/common/script/fns/randomDrop';
|
||||
import i18n from '../../../website/common/script/i18n';
|
||||
import {
|
||||
generateUser,
|
||||
generateTodo,
|
||||
@@ -144,5 +145,148 @@ describe('common.fns.randomDrop', () => {
|
||||
expect(acceptableDrops).to.contain(user._tmp.drop.key); // always Desert
|
||||
});
|
||||
});
|
||||
|
||||
context('drop cap notification', () => {
|
||||
let analytics;
|
||||
const req = {};
|
||||
let isSubscribedStub;
|
||||
|
||||
beforeEach(() => {
|
||||
user.addNotification = () => {};
|
||||
sandbox.stub(user, 'addNotification');
|
||||
user.isSubscribed = () => {};
|
||||
isSubscribedStub = sandbox.stub(user, 'isSubscribed');
|
||||
isSubscribedStub.returns(false);
|
||||
analytics = { track () {} };
|
||||
sandbox.stub(analytics, 'track');
|
||||
});
|
||||
|
||||
it('sends a notification if A/B test is enabled when drop cap is reached', () => {
|
||||
user._ABtests.dropCapNotif = 'drop-cap-notif-enabled';
|
||||
predictableRandom.returns(0.1);
|
||||
|
||||
// Max Drop Count is 5
|
||||
expect(user.items.lastDrop.count).to.equal(0);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
expect(user.items.lastDrop.count).to.equal(5);
|
||||
expect(user.addNotification).to.be.calledOnce;
|
||||
expect(user.addNotification).to.be.calledWith('DROP_CAP_REACHED', {
|
||||
message: i18n.t('dropCapReached'),
|
||||
items: 5,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not send a notification if user is enrolled in disabled A/B test group', () => {
|
||||
user._ABtests.dropCapNotif = 'drop-cap-notif-disabled';
|
||||
predictableRandom.returns(0.1);
|
||||
|
||||
// Max Drop Count is 5
|
||||
expect(user.items.lastDrop.count).to.equal(0);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
expect(user.items.lastDrop.count).to.equal(5);
|
||||
expect(user.addNotification).to.not.be.called;
|
||||
});
|
||||
|
||||
it('does not send a notification if user is enrolled in disabled A/B test group', () => {
|
||||
user._ABtests.dropCapNotif = 'drop-cap-notif-not-enrolled';
|
||||
predictableRandom.returns(0.1);
|
||||
|
||||
// Max Drop Count is 5
|
||||
expect(user.items.lastDrop.count).to.equal(0);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
expect(user.items.lastDrop.count).to.equal(5);
|
||||
expect(user.addNotification).to.not.be.called;
|
||||
});
|
||||
|
||||
it('does not send a notification if drop cap is not reached', () => {
|
||||
user._ABtests.dropCapNotif = 'drop-cap-notif-enabled';
|
||||
predictableRandom.returns(0.1);
|
||||
|
||||
// Max Drop Count is 5
|
||||
expect(user.items.lastDrop.count).to.equal(0);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
expect(user.items.lastDrop.count).to.equal(4);
|
||||
expect(user.addNotification).to.not.be.called;
|
||||
});
|
||||
|
||||
it('does not send a notification if user is subscribed', () => {
|
||||
user._ABtests.dropCapNotif = 'drop-cap-notif-enabled';
|
||||
predictableRandom.returns(0.1);
|
||||
isSubscribedStub.returns(true);
|
||||
|
||||
// Max Drop Count is 5
|
||||
expect(user.items.lastDrop.count).to.equal(0);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
expect(user.items.lastDrop.count).to.equal(5);
|
||||
expect(user.addNotification).to.not.be.called;
|
||||
});
|
||||
|
||||
it('tracks drop cap reached event for enrolled users (notification enabled)', () => {
|
||||
user._ABtests.dropCapNotif = 'drop-cap-notif-enabled';
|
||||
predictableRandom.returns(0.1);
|
||||
isSubscribedStub.returns(true);
|
||||
|
||||
// Max Drop Count is 5
|
||||
expect(user.items.lastDrop.count).to.equal(0);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
expect(user.items.lastDrop.count).to.equal(5);
|
||||
expect(analytics.track).to.be.calledWith('drop cap reached');
|
||||
});
|
||||
|
||||
it('tracks drop cap reached event for enrolled users (notification disabled)', () => {
|
||||
user._ABtests.dropCapNotif = 'drop-cap-notif-disabled';
|
||||
predictableRandom.returns(0.1);
|
||||
isSubscribedStub.returns(true);
|
||||
|
||||
// Max Drop Count is 5
|
||||
expect(user.items.lastDrop.count).to.equal(0);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
expect(user.items.lastDrop.count).to.equal(5);
|
||||
expect(analytics.track).to.be.calledWith('drop cap reached');
|
||||
});
|
||||
|
||||
it('does not track drop cap reached event for users not enrolled in A/B test', () => {
|
||||
user._ABtests.dropCapNotif = 'drop-cap-notif-not-enrolled';
|
||||
predictableRandom.returns(0.1);
|
||||
isSubscribedStub.returns(true);
|
||||
|
||||
// Max Drop Count is 5
|
||||
expect(user.items.lastDrop.count).to.equal(0);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
expect(user.items.lastDrop.count).to.equal(5);
|
||||
expect(analytics.track).to.not.be.calledWith('drop cap reached');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../helpers/common.helper';
|
||||
import {
|
||||
UNEQUIP_ALL,
|
||||
UNEQUIP_BACKGROUND,
|
||||
UNEQUIP_COSTUME,
|
||||
UNEQUIP_EQUIPPED,
|
||||
UNEQUIP_PET_MOUNT,
|
||||
unEquipByType,
|
||||
} from '../../../website/common/script/ops/unequip';
|
||||
|
||||
describe('shared.ops.unequip', () => {
|
||||
let user;
|
||||
|
||||
beforeEach(() => {
|
||||
user = generateUser({
|
||||
preferences: {
|
||||
background: 'violet',
|
||||
},
|
||||
items: {
|
||||
currentMount: 'BearCub-Base',
|
||||
currentPet: 'BearCub-Base',
|
||||
gear: {
|
||||
owned: {
|
||||
weapon_warrior_0: true,
|
||||
weapon_warrior_1: true,
|
||||
weapon_warrior_2: true,
|
||||
weapon_wizard_1: true,
|
||||
weapon_wizard_2: true,
|
||||
shield_base_0: true,
|
||||
shield_warrior_1: true,
|
||||
},
|
||||
equipped: {
|
||||
weapon: 'weapon_warrior_2',
|
||||
shield: 'shield_warrior_1',
|
||||
},
|
||||
costume: {
|
||||
weapon: 'weapon_warrior_2',
|
||||
shield: 'shield_warrior_1',
|
||||
},
|
||||
},
|
||||
},
|
||||
stats: { gp: 200 },
|
||||
});
|
||||
});
|
||||
|
||||
context('Gear', () => {
|
||||
it('should unequip all battle gear items', () => {
|
||||
unEquipByType(user, { params: { type: UNEQUIP_EQUIPPED } });
|
||||
|
||||
expect(user.items.gear.equipped.weapon).to.eq('weapon_base_0');
|
||||
expect(user.items.gear.equipped.shield).to.eq('shield_base_0');
|
||||
});
|
||||
});
|
||||
|
||||
context('Costume', () => {
|
||||
it('should unequip all costume items', () => {
|
||||
unEquipByType(user, { params: { type: UNEQUIP_COSTUME } });
|
||||
|
||||
expect(user.items.gear.costume.weapon).to.eq('weapon_base_0');
|
||||
expect(user.items.gear.costume.shield).to.eq('shield_base_0');
|
||||
});
|
||||
});
|
||||
|
||||
context('Pet and Mount', () => {
|
||||
it('should unequip Pet and Mount', () => {
|
||||
unEquipByType(user, { params: { type: UNEQUIP_PET_MOUNT } });
|
||||
|
||||
expect(user.items.currentMount).to.eq('');
|
||||
expect(user.items.currentPet).to.eq('');
|
||||
});
|
||||
});
|
||||
|
||||
context('Background', () => {
|
||||
it('should unequip Background', () => {
|
||||
unEquipByType(user, { params: { type: UNEQUIP_BACKGROUND } });
|
||||
|
||||
expect(user.preferences.background).to.eq('');
|
||||
});
|
||||
});
|
||||
|
||||
context('All Items', () => {
|
||||
it('should unequip all Items', () => {
|
||||
unEquipByType(user, { params: { type: UNEQUIP_ALL } });
|
||||
|
||||
expect(user.items.gear.equipped.weapon).to.eq('weapon_base_0');
|
||||
expect(user.items.gear.equipped.shield).to.eq('shield_base_0');
|
||||
|
||||
expect(user.items.gear.costume.weapon).to.eq('weapon_base_0');
|
||||
expect(user.items.gear.costume.shield).to.eq('shield_base_0');
|
||||
|
||||
expect(user.items.currentMount).to.eq('');
|
||||
expect(user.items.currentPet).to.eq('');
|
||||
expect(user.preferences.background).to.eq('');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -50,4 +50,5 @@ function loadStories () {
|
||||
req.keys().forEach(filename => req(filename));
|
||||
}
|
||||
|
||||
|
||||
configure(loadStories, module);
|
||||
|
||||
@@ -18,18 +18,18 @@
|
||||
"@storybook/addon-links": "^5.3.19",
|
||||
"@storybook/addon-notes": "^5.3.21",
|
||||
"@storybook/vue": "^5.3.19",
|
||||
"@vue/cli-plugin-babel": "^4.5.6",
|
||||
"@vue/cli-plugin-eslint": "^4.5.6",
|
||||
"@vue/cli-plugin-router": "^4.5.6",
|
||||
"@vue/cli-plugin-unit-mocha": "^4.5.6",
|
||||
"@vue/cli-service": "^4.5.6",
|
||||
"@vue/cli-plugin-babel": "^4.5.8",
|
||||
"@vue/cli-plugin-eslint": "^4.5.8",
|
||||
"@vue/cli-plugin-router": "^4.5.8",
|
||||
"@vue/cli-plugin-unit-mocha": "^4.5.8",
|
||||
"@vue/cli-service": "^4.5.8",
|
||||
"@vue/test-utils": "1.0.0-beta.29",
|
||||
"amplitude-js": "^7.2.2",
|
||||
"axios": "^0.19.2",
|
||||
"amplitude-js": "^7.3.1",
|
||||
"axios": "^0.21.0",
|
||||
"axios-progress-bar": "^1.2.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"bootstrap": "^4.5.2",
|
||||
"bootstrap-vue": "^2.17.3",
|
||||
"bootstrap": "^4.5.3",
|
||||
"bootstrap-vue": "^2.18.1",
|
||||
"chai": "^4.1.2",
|
||||
"core-js": "^3.6.5",
|
||||
"eslint": "^6.8.0",
|
||||
@@ -37,28 +37,28 @@
|
||||
"eslint-plugin-mocha": "^5.3.0",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"habitica-markdown": "^3.0.0",
|
||||
"hellojs": "^1.18.4",
|
||||
"hellojs": "^1.18.6",
|
||||
"inspectpack": "^4.5.2",
|
||||
"intro.js": "^2.9.3",
|
||||
"jquery": "^3.5.1",
|
||||
"lodash": "^4.17.20",
|
||||
"moment": "^2.29.0",
|
||||
"moment": "^2.29.1",
|
||||
"nconf": "^0.10.0",
|
||||
"sass": "^1.26.11",
|
||||
"sass": "^1.28.0",
|
||||
"sass-loader": "^8.0.2",
|
||||
"smartbanner.js": "^1.16.0",
|
||||
"svg-inline-loader": "^0.8.2",
|
||||
"svg-url-loader": "^6.0.0",
|
||||
"svgo": "^1.3.2",
|
||||
"svgo-loader": "^2.2.1",
|
||||
"uuid": "^8.3.0",
|
||||
"uuid": "^8.3.1",
|
||||
"validator": "^13.1.17",
|
||||
"vue": "^2.6.12",
|
||||
"vue-cli-plugin-storybook": "^0.6.1",
|
||||
"vue-mugen-scroll": "^0.2.6",
|
||||
"vue-router": "^3.4.5",
|
||||
"vue-router": "^3.4.8",
|
||||
"vue-template-compiler": "^2.6.12",
|
||||
"vuedraggable": "^2.24.1",
|
||||
"vuedraggable": "^2.24.3",
|
||||
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#153d339e4dbebb73733658aeda1d5b7fcc55b0a0",
|
||||
"webpack": "^4.44.2"
|
||||
}
|
||||
|
||||
@@ -540,5 +540,6 @@ export default {
|
||||
<style src="@/assets/css/sprites/spritesmith-main-26.css"></style>
|
||||
<style src="@/assets/css/sprites/spritesmith-main-27.css"></style>
|
||||
<style src="@/assets/css/sprites/spritesmith-main-28.css"></style>
|
||||
<style src="@/assets/css/sprites/spritesmith-main-29.css"></style>
|
||||
<style src="@/assets/css/sprites.css"></style>
|
||||
<style src="smartbanner.js/dist/smartbanner.min.css"></style>
|
||||
|
||||
@@ -1,48 +1,66 @@
|
||||
.promo_armoire_backgrounds_202009 {
|
||||
.promo_armoire_backgrounds_202010 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -452px;
|
||||
background-position: 0px -639px;
|
||||
width: 423px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_fall_customizations {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -244px;
|
||||
background-position: -532px 0px;
|
||||
width: 336px;
|
||||
height: 207px;
|
||||
}
|
||||
.promo_fall_festival_2019 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -532px 0px;
|
||||
background-position: 0px -449px;
|
||||
width: 360px;
|
||||
height: 189px;
|
||||
}
|
||||
.promo_fall_festival_2020 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -532px -190px;
|
||||
background-position: -361px -449px;
|
||||
width: 360px;
|
||||
height: 174px;
|
||||
}
|
||||
.promo_mystery_202009 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -600px;
|
||||
background-position: -869px -296px;
|
||||
width: 282px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_mystery_202010 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -283px -600px;
|
||||
background-position: -869px -444px;
|
||||
width: 282px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_sandy_sidekicks_bundle {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -869px -148px;
|
||||
width: 420px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_skeleton_achievements {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -244px;
|
||||
width: 366px;
|
||||
height: 204px;
|
||||
}
|
||||
.promo_spooky_sparkles {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -424px -639px;
|
||||
width: 423px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_take_this {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -893px -170px;
|
||||
background-position: -1152px -296px;
|
||||
width: 96px;
|
||||
height: 69px;
|
||||
}
|
||||
.promo_vampire_potions {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -424px -452px;
|
||||
background-position: -869px 0px;
|
||||
width: 423px;
|
||||
height: 147px;
|
||||
}
|
||||
@@ -54,13 +72,13 @@
|
||||
}
|
||||
.scene_squall {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -893px 0px;
|
||||
background-position: -532px -208px;
|
||||
width: 141px;
|
||||
height: 169px;
|
||||
}
|
||||
.scene_strength {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -337px -244px;
|
||||
background-position: -869px -592px;
|
||||
width: 192px;
|
||||
height: 129px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
.Pet-Yarn-Golden {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: 0px 0px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Yarn-Red {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: -82px 0px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Yarn-Shade {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: -164px 0px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Yarn-Skeleton {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: 0px -100px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Yarn-White {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: -82px -100px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Yarn-Zombie {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: -164px -100px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet_HatchingPotion_Amber {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: -246px 0px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Aquatic {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: -246px -69px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Aurora {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: 0px -200px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_AutumnLeaf {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: -69px -200px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Base {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: -138px -200px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_BirchBark {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: -207px -200px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Bronze {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: -315px 0px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Celestial {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: -315px -69px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_CottonCandyBlue {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: -315px -138px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_CottonCandyPink {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: 0px -269px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Cupid {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: -69px -269px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Desert {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: -138px -269px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Ember {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: -207px -269px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Fairy {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: -276px -269px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Floral {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: -384px 0px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Fluorite {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: -384px -69px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Frost {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: -384px -138px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Ghost {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: -384px -207px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Glass {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: 0px -338px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Glow {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: -69px -338px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Golden {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: -138px -338px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Holly {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: -207px -338px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_IcySnow {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: -276px -338px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Peppermint {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: -345px -338px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Purple {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: -453px 0px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Rainbow {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: -453px -69px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Red {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: -453px -138px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_RoseQuartz {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: -453px -207px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_RoyalPurple {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: -453px -276px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Ruby {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: 0px -407px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_SandSculpture {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: -69px -407px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Shade {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: -138px -407px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Shadow {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: -207px -407px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Shimmer {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: -276px -407px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Silver {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: -345px -407px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Skeleton {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: -414px -407px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Spooky {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: -522px 0px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_StarryNight {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: -522px -69px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Sunshine {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: -522px -138px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Thunderstorm {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: -522px -207px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Turquoise {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: -522px -276px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Vampire {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: -522px -345px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Watery {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: 0px -476px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_White {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: -69px -476px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Zombie {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-29.png');
|
||||
background-position: -138px -476px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 472 KiB After Width: | Height: | Size: 466 KiB |
|
Before Width: | Height: | Size: 565 KiB After Width: | Height: | Size: 545 KiB |
|
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 357 KiB After Width: | Height: | Size: 322 KiB |
|
Before Width: | Height: | Size: 346 KiB After Width: | Height: | Size: 352 KiB |
|
Before Width: | Height: | Size: 154 KiB After Width: | Height: | Size: 173 KiB |
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 168 KiB |
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 311 KiB After Width: | Height: | Size: 348 KiB |
|
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 174 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 158 KiB After Width: | Height: | Size: 151 KiB |
|
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 181 KiB After Width: | Height: | Size: 161 KiB |
|
Before Width: | Height: | Size: 172 KiB After Width: | Height: | Size: 184 KiB |
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 173 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 153 KiB After Width: | Height: | Size: 150 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 12 KiB |
@@ -19,6 +19,6 @@
|
||||
top: -16px !important;
|
||||
}
|
||||
|
||||
.Pet.Pet-FlyingPig-Veggie {
|
||||
.Pet.Pet-FlyingPig-Veggie, .Pet.Pet-FlyingPig-Dessert {
|
||||
top: -28px !important;
|
||||
}
|
||||
|
||||
@@ -2,13 +2,15 @@
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
line-height: 1.33;
|
||||
color: $gray-200;
|
||||
padding: 4px 8px;
|
||||
text-align: center;
|
||||
color: $gray-100;
|
||||
|
||||
padding: 0.25rem 0.5rem;
|
||||
box-shadow: 0 1px 3px 0 rgba($black, 0.12), 0 1px 2px 0 rgba($black, 0.24);
|
||||
}
|
||||
|
||||
.badge-pill {
|
||||
border-radius: 100px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.badge-round {
|
||||
@@ -18,7 +20,7 @@
|
||||
}
|
||||
|
||||
.badge-default {
|
||||
background: $gray-500;
|
||||
background: $gray-600;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
font-weight: bold;
|
||||
line-height: 1.71;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.219rem 1rem;
|
||||
padding: 0.219rem 0.75rem;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 1px 3px 0 rgba($black, 0.12), 0 1px 2px 0 rgba($black, 0.24);
|
||||
color: $white;
|
||||
@@ -22,7 +22,7 @@
|
||||
border-color: $purple-400;
|
||||
}
|
||||
|
||||
&:active, &.active:not(.btn-flat), &:disabled, &.disabled {
|
||||
&:active, &.active:not(.btn-flat) {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@@ -30,6 +30,13 @@
|
||||
cursor: default;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
&.with-icon {
|
||||
height: 2rem; // otherwise would something set the height to 33px
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-front {
|
||||
@@ -41,34 +48,49 @@
|
||||
.btn-primary {
|
||||
background: $purple-200;
|
||||
border: 1px solid transparent;
|
||||
|
||||
&:hover:not(:disabled):not(.disabled) {
|
||||
background: #5d3b9c;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
--icon-color: #{$purple-500};
|
||||
|
||||
&:focus {
|
||||
background: $purple-200;
|
||||
border-color: $purple-400;
|
||||
--icon-color: #{$white};
|
||||
}
|
||||
|
||||
&:not(:disabled):not(.disabled) {
|
||||
&:hover {
|
||||
background: #5d3b9c;
|
||||
border: 1px solid transparent;
|
||||
|
||||
--icon-color: #{$white};
|
||||
}
|
||||
|
||||
&:active, &.active {
|
||||
background: $purple-200;
|
||||
border: 1px solid transparent;
|
||||
|
||||
--icon-color: #{$white};
|
||||
}
|
||||
|
||||
&:active:focus, &.active:focus {
|
||||
box-shadow: none;
|
||||
border-color: $purple-400;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:disabled):not(.disabled):active, &:not(:disabled):not(.disabled).active {
|
||||
background: $purple-200;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
&:disabled, &.disabled {
|
||||
background: $purple-200;
|
||||
background: $gray-700;
|
||||
border: 1px solid transparent;
|
||||
cursor: default;
|
||||
opacity: 0.75;
|
||||
color: $gray-50;
|
||||
|
||||
--icon-color: #{$gray-300};
|
||||
}
|
||||
|
||||
|
||||
&.with-icon {
|
||||
.svg-icon.color {
|
||||
color: var(--icon-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,29 +102,27 @@
|
||||
border: 1px solid transparent;
|
||||
color: $gray-50;
|
||||
|
||||
--icon-color: #{$gray-200};
|
||||
|
||||
&:focus, &:active {
|
||||
color: $gray-50;
|
||||
background: $white;
|
||||
border-color: $purple-400;
|
||||
|
||||
--icon-color: #{$purple-300};
|
||||
}
|
||||
|
||||
&:not(:disabled):not(.disabled) {
|
||||
&:active:focus,
|
||||
&.active:focus {
|
||||
&:active, &.active {
|
||||
color: $purple-300;
|
||||
box-shadow: none;
|
||||
border-color: $purple-400;
|
||||
}
|
||||
--icon-color: #{$purple-300};
|
||||
|
||||
&:active,
|
||||
&.active {
|
||||
color: $purple-300;
|
||||
|
||||
&.dropdown-toggle {
|
||||
color: $gray-50;
|
||||
&:focus {
|
||||
color: $purple-300;
|
||||
box-shadow: none;
|
||||
border-color: $purple-400;
|
||||
}
|
||||
|
||||
|
||||
background: $white;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
@@ -110,12 +130,10 @@
|
||||
&:hover {
|
||||
color: $purple-300;
|
||||
|
||||
&.dropdown-toggle {
|
||||
color: $gray-50;
|
||||
}
|
||||
|
||||
background: $white !important;
|
||||
border: 1px solid transparent;
|
||||
|
||||
--icon-color: #{$purple-300};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,6 +143,14 @@
|
||||
border: 1px solid transparent;
|
||||
cursor: default;
|
||||
opacity: 0.75;
|
||||
|
||||
--icon-color: #{$gray-300};
|
||||
}
|
||||
|
||||
&.with-icon {
|
||||
.svg-icon.color {
|
||||
color: var(--icon-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
.dropdown > .btn {
|
||||
padding: 0.25rem 0.75rem;
|
||||
padding: 0.219rem 0.75rem;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
line-height: 1.43;
|
||||
}
|
||||
|
||||
.dropdown-toggle:hover {
|
||||
@@ -25,6 +24,8 @@
|
||||
border-right: 5px solid transparent;
|
||||
border-left: 5px solid transparent;
|
||||
vertical-align: 0;
|
||||
margin-left: 1rem;
|
||||
margin-bottom: 0.1rem;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
@@ -44,7 +45,7 @@
|
||||
|
||||
font-size: 14px;
|
||||
line-height: 1.71;
|
||||
color: $gray-50;
|
||||
color: $gray-50 !important;
|
||||
cursor: pointer;
|
||||
|
||||
&:focus {
|
||||
@@ -54,16 +55,16 @@
|
||||
|
||||
|
||||
&:active, &:hover, &:focus, &.active {
|
||||
background-color: rgba($purple-600, 0.32);
|
||||
color: $purple-200;
|
||||
background-color: rgba($purple-600, 0.25) !important;
|
||||
color: $purple-300 !important;
|
||||
}
|
||||
|
||||
&.dropdown-inactive {
|
||||
cursor: default;
|
||||
|
||||
&:active, &:hover, &.active {
|
||||
background-color: inherit;
|
||||
color: inherit;
|
||||
background-color: inherit !important;
|
||||
color: inherit !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,10 +76,17 @@
|
||||
.dropdown-label {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
line-height: 1.43;
|
||||
color: $gray-10;
|
||||
line-height: 1.71;
|
||||
margin-right: 20px;
|
||||
margin-left: 20px;
|
||||
|
||||
height: 1.5rem;
|
||||
font-stretch: normal;
|
||||
font-style: normal;
|
||||
text-align: right;
|
||||
|
||||
margin-top: calc(0.25rem + 1px); // Padding of the dropdown buttons + button border width
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.dropdown-icon-item {
|
||||
|
||||