Compare commits
230 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7d7654bb04 | |||
| f7e9b467a5 | |||
| 86fce69928 | |||
| 1c45d170dd | |||
| be107f02d3 | |||
| c4b16f86db | |||
| cc7d065445 | |||
| e4edab2b9d | |||
| fb1ea935e6 | |||
| 9795597c79 | |||
| c36f01773f | |||
| a4eabc05e7 | |||
| ae26cc3244 | |||
| 9fe28c6d51 | |||
| 34983f1221 | |||
| 8f5a0cfe79 | |||
| a00a8cced8 | |||
| 1aceccc6c4 | |||
| 10c4baba4a | |||
| 56cdc94860 | |||
| 1346b71b81 | |||
| 023173fada | |||
| b5182ed7d4 | |||
| 315604073b | |||
| 3a5e93f82e | |||
| a45b04b1cf | |||
| a49f95e1a5 | |||
| 0ac12cb6a5 | |||
| cd90a281c2 | |||
| e223311aac | |||
| bdb3cf25c1 | |||
| 0bb69665f8 | |||
| 7a52386daf | |||
| 6959d8b8d4 | |||
| efb4efd768 | |||
| 40ba81a5d9 | |||
| 7e256c2d96 | |||
| 5ea1211571 | |||
| 73a91c4f3c | |||
| 185e695e31 | |||
| 6fdce9f51c | |||
| f905da7d57 | |||
| 33a69452e8 | |||
| a049b56a63 | |||
| 1e85ec9007 | |||
| 1d69f44e7e | |||
| 53a15f9fa0 | |||
| aa1fcb4956 | |||
| 97a1d0f474 | |||
| 1cd1d45410 | |||
| 9a13594cca | |||
| 6c36ebc952 | |||
| b6371971a2 | |||
| 25caf1f850 | |||
| 3f1e8ceae4 | |||
| 1224d6d0c9 | |||
| 11b1bf01cc | |||
| 393aef1b04 | |||
| 3466e62aa5 | |||
| 2565772093 | |||
| 11ebc6c397 | |||
| afd82784b2 | |||
| ad20d451b3 | |||
| 29046cc2d5 | |||
| e8a19c32fe | |||
| 37fc8850a8 | |||
| 302fa9b4b9 | |||
| b2102657af | |||
| 0b16ec775e | |||
| 9edac6d56e | |||
| a5bfec0c95 | |||
| 891bb2e2ae | |||
| 297cce06fc | |||
| c5587fcbdb | |||
| 6314a53136 | |||
| 9379325731 | |||
| 59c4b32d69 | |||
| b108b047cd | |||
| 3c394e7448 | |||
| e8eb014647 | |||
| 0f9f8a7723 | |||
| 45efbc1e4b | |||
| faec0bc979 | |||
| e597906f35 | |||
| 42de623fc6 | |||
| fb74f59ae5 | |||
| 6395070eb6 | |||
| d0033322f7 | |||
| 76b5332d78 | |||
| e8f9c4953e | |||
| cf3a092f06 | |||
| ae9bca3ac7 | |||
| 194f2d512a | |||
| f70f7b2a1c | |||
| 9a6f98b0b5 | |||
| bfd960b7b6 | |||
| 91d75bae29 | |||
| 8983e7fea2 | |||
| bff88434fd | |||
| a1d31e37a3 | |||
| c09a014d8d | |||
| ca8b232e01 | |||
| af7ce402e5 | |||
| e82d415d5f | |||
| 3839fd276b | |||
| 987801fa83 | |||
| f9c288acdf | |||
| 49b8ec22f4 | |||
| d3a4c3eaea | |||
| 2dea0c0b9c | |||
| 6a6963103b | |||
| 62986426ba | |||
| 95c8302c9a | |||
| cf75888ffe | |||
| f939208cdb | |||
| ffe5340cf2 | |||
| 750ce0d6cc | |||
| 9af9c553d4 | |||
| 6705ce0dec | |||
| 0fb421897e | |||
| d20d254c6f | |||
| 7d78a7b320 | |||
| 2362976f14 | |||
| 99b63456c8 | |||
| e0b8cfbaa7 | |||
| 4ca4fd9ae7 | |||
| bb84b6f6c2 | |||
| da8346ea50 | |||
| 451df7d50d | |||
| 3a359cc057 | |||
| 66b2b47f87 | |||
| 8992542125 | |||
| 23bd51d486 | |||
| b4efe11e6a | |||
| 46822ecbae | |||
| 85c979a22d | |||
| ed02913e9d | |||
| 84ecb7c701 | |||
| dbd283514c | |||
| 12f802709b | |||
| a504f69b8a | |||
| ca8e6c74f6 | |||
| b9dbc0f829 | |||
| 789248a8a4 | |||
| 0731dcd453 | |||
| 960985dee5 | |||
| eb991ae5eb | |||
| eaad244181 | |||
| a859dbd646 | |||
| 106290a11e | |||
| 3fa0bac36f | |||
| b40dee9e68 | |||
| 6a9ffae758 | |||
| f1ce7f23c3 | |||
| f093634cb2 | |||
| 5f0672f3a6 | |||
| eca1d8501c | |||
| 3fc0fec95e | |||
| f4f1eac92d | |||
| 0e679f0fea | |||
| d55d3fc56d | |||
| 9a6347afb6 | |||
| 20fa676fae | |||
| 4e627aad41 | |||
| 27d8ad32e6 | |||
| 39fd60267c | |||
| 645f40f7e0 | |||
| 2842087a43 | |||
| 3c4d8949de | |||
| 2a7f4c0551 | |||
| 5212e72e77 | |||
| d49c89eff8 | |||
| dc303fb1de | |||
| 1883a0aca3 | |||
| 06339a1504 | |||
| 71b5db1461 | |||
| 8420a2cf23 | |||
| 67b19f0658 | |||
| 0f34103a58 | |||
| e2e320a3b6 | |||
| 2d4516eaa7 | |||
| 1c7e1e5aff | |||
| 44a7a0159b | |||
| 2091c407b0 | |||
| 81ce84db31 | |||
| 319621d4fe | |||
| 45f7be0266 | |||
| 035a7f124b | |||
| d0ba8b7d30 | |||
| a32622c81f | |||
| 5e4efb71a6 | |||
| 55c4e37f68 | |||
| b441cfa9f3 | |||
| 159b4857ba | |||
| aa518862e3 | |||
| a2a2fea40f | |||
| 3c1c92dd68 | |||
| 8ab50dc316 | |||
| 25603634db | |||
| ef5d6d7889 | |||
| f15a32255b | |||
| a7fc1aa7a7 | |||
| e86b23a98a | |||
| 84d76a2ebc | |||
| 9988b1e82d | |||
| da4dd07de1 | |||
| 4d88787ba0 | |||
| 6e9ab1dbbb | |||
| 853b9313f6 | |||
| a7c814262b | |||
| f02992faa5 | |||
| d98614d403 | |||
| 0482941934 | |||
| cf00bddd6f | |||
| dfd79c9c1a | |||
| 2741721161 | |||
| 2ca3c225c4 | |||
| 9ceb45b058 | |||
| aedabd2266 | |||
| 60a12e8090 | |||
| 33e9e7b647 | |||
| 4881d870c7 | |||
| 5b7b87e524 | |||
| 90f732f545 | |||
| 6ab6fdf1f0 | |||
| 5653030c64 | |||
| 3ce74a3fd1 | |||
| 907dd406f3 | |||
| 42687335af | |||
| 3797d29835 |
@@ -1,3 +1,2 @@
|
||||
node_modules
|
||||
.git
|
||||
website
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[//]: # (Note: See http://habitica.fandom.com/wiki/Using_Your_Local_Install_to_Modify_Habitica%27s_Website_and_API for more info)
|
||||
|
||||
[//]: # (Put Issue # here, if applicable. This will automatically close the issue if your PR is merged in)
|
||||
Fixes put_#_and_issue_numer_here
|
||||
Fixes put_#_and_issue_number_here
|
||||
|
||||
### Changes
|
||||
[//]: # (Describe the changes that were made in detail here. Include pictures if necessary)
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
FROM node:12
|
||||
WORKDIR /code
|
||||
COPY package*.json /code/
|
||||
RUN npm install
|
||||
RUN npm install -g gulp-cli mocha
|
||||
FROM node:12
|
||||
|
||||
# Install global packages
|
||||
RUN npm install -g gulp-cli mocha
|
||||
|
||||
# Copy Habitica code into container and install dependencies
|
||||
WORKDIR /usr/src/habitica
|
||||
COPY . /usr/src/habitica
|
||||
|
||||
RUN npm install
|
||||
RUN npm run postinstall
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
/* eslint-disable no-console */
|
||||
const MIGRATION_NAME = '20191127_harvest_feast';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { model as User } from '../../../website/server/models/user';
|
||||
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
async function updateUser (user) {
|
||||
count++;
|
||||
|
||||
const set = {};
|
||||
let inc;
|
||||
let push;
|
||||
|
||||
set.migration = MIGRATION_NAME;
|
||||
|
||||
if (typeof user.items.gear.owned.head_special_turkeyHelmGilded !== 'undefined') {
|
||||
inc = {
|
||||
'items.food.Pie_Base': 1,
|
||||
'items.food.Pie_CottonCandyBlue': 1,
|
||||
'items.food.Pie_CottonCandyPink': 1,
|
||||
'items.food.Pie_Desert': 1,
|
||||
'items.food.Pie_Golden': 1,
|
||||
'items.food.Pie_Red': 1,
|
||||
'items.food.Pie_Shade': 1,
|
||||
'items.food.Pie_Skeleton': 1,
|
||||
'items.food.Pie_Zombie': 1,
|
||||
'items.food.Pie_White': 1,
|
||||
}
|
||||
} else if (typeof user.items.gear.owned.armor_special_turkeyArmorBase !== 'undefined') {
|
||||
set['items.gear.owned.head_special_turkeyHelmGilded'] = false;
|
||||
set['items.gear.owned.armor_special_turkeyArmorGilded'] = false;
|
||||
set['items.gear.owned.back_special_turkeyTailGilded'] = false;
|
||||
push = [
|
||||
{
|
||||
type: 'marketGear',
|
||||
path: 'gear.flat.head_special_turkeyHelmGilded',
|
||||
_id: uuid(),
|
||||
},
|
||||
{
|
||||
type: 'marketGear',
|
||||
path: 'gear.flat.armor_special_turkeyArmorGilded',
|
||||
_id: uuid(),
|
||||
},
|
||||
{
|
||||
type: 'marketGear',
|
||||
path: 'gear.flat.back_special_turkeyTailGilded',
|
||||
_id: uuid(),
|
||||
},
|
||||
];
|
||||
} else if (user.items && user.items.mounts && user.items.mounts['Turkey-Gilded']) {
|
||||
set['items.gear.owned.head_special_turkeyHelmBase'] = false;
|
||||
set['items.gear.owned.armor_special_turkeyArmorBase'] = false;
|
||||
set['items.gear.owned.back_special_turkeyTailBase'] = false;
|
||||
push = [
|
||||
{
|
||||
type: 'marketGear',
|
||||
path: 'gear.flat.head_special_turkeyHelmBase',
|
||||
_id: uuid(),
|
||||
},
|
||||
{
|
||||
type: 'marketGear',
|
||||
path: 'gear.flat.armor_special_turkeyArmorBase',
|
||||
_id: uuid(),
|
||||
},
|
||||
{
|
||||
type: 'marketGear',
|
||||
path: 'gear.flat.back_special_turkeyTailBase',
|
||||
_id: uuid(),
|
||||
},
|
||||
];
|
||||
} else if (user.items && user.items.pets && user.items.pets['Turkey-Gilded']) {
|
||||
set['items.mounts.Turkey-Gilded'] = true;
|
||||
} else if (user.items && user.items.mounts && user.items.mounts['Turkey-Base']) {
|
||||
set['items.pets.Turkey-Gilded'] = 5;
|
||||
} else if (user.items && user.items.pets && user.items.pets['Turkey-Base']) {
|
||||
set['items.mounts.Turkey-Base'] = true;
|
||||
} else {
|
||||
set['items.pets.Turkey-Base'] = 5;
|
||||
}
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
|
||||
if (inc) {
|
||||
return await User.update({_id: user._id}, {$inc: inc, $set: set}).exec();
|
||||
} else if (push) {
|
||||
return await User.update({_id: user._id}, {$set: set, $push: {pinnedItems: {$each: push}}}).exec();
|
||||
} else {
|
||||
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('2019-11-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,82 @@
|
||||
/* eslint-disable no-console */
|
||||
const MIGRATION_NAME = '20191210_pet_color_achievements';
|
||||
import { model as User } from '../../../website/server/models/user';
|
||||
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
async function updateUser (user) {
|
||||
count++;
|
||||
|
||||
let set = {
|
||||
migration: MIGRATION_NAME,
|
||||
};
|
||||
|
||||
if (user && user.items && user.items.pets) {
|
||||
const pets = user.items.pets;
|
||||
if (pets['Wolf-White'] > 0
|
||||
&& pets['TigerCub-White'] > 0
|
||||
&& pets['PandaCub-White'] > 0
|
||||
&& pets['LionCub-White'] > 0
|
||||
&& pets['Fox-White'] > 0
|
||||
&& pets['FlyingPig-White'] > 0
|
||||
&& pets['Dragon-White'] > 0
|
||||
&& pets['Cactus-White'] > 0
|
||||
&& pets['BearCub-White'] > 0) {
|
||||
set['achievements.primedForPainting'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (user && user.items && user.items.mounts) {
|
||||
const mounts = user.items.mounts;
|
||||
if (mounts['Wolf-White']
|
||||
&& mounts['TigerCub-White']
|
||||
&& mounts['PandaCub-White']
|
||||
&& mounts['LionCub-White']
|
||||
&& mounts['Fox-White']
|
||||
&& mounts['FlyingPig-White']
|
||||
&& mounts['Dragon-White']
|
||||
&& mounts['Cactus-White']
|
||||
&& mounts['BearCub-White'] ) {
|
||||
set['achievements.pearlyPro'] = 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('2019-12-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
|
||||
}
|
||||
};
|
||||
@@ -1,110 +0,0 @@
|
||||
import monk from 'monk';
|
||||
import nconf from 'nconf';
|
||||
|
||||
const migrationName = 'mystery-items-201808.js'; // Update per month
|
||||
const authorName = 'Sabe'; // in case script author needs to know when their ...
|
||||
const authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done
|
||||
|
||||
/*
|
||||
* Award this month's mystery items to subscribers
|
||||
*/
|
||||
const MYSTERY_ITEMS = ['armor_mystery_201810', 'head_mystery_201810'];
|
||||
const CONNECTION_STRING = nconf.get('MIGRATION_CONNECT_STRING');
|
||||
|
||||
let dbUsers = monk(CONNECTION_STRING).get('users', { castIds: false });
|
||||
let UserNotification = require('../../website/server/models/userNotification').model;
|
||||
|
||||
function processUsers (lastId) {
|
||||
// specify a query to limit the affected users (empty for all users):
|
||||
let query = {
|
||||
migration: {$ne: migrationName},
|
||||
'purchased.plan.customerId': { $ne: null },
|
||||
$or: [
|
||||
{ 'purchased.plan.dateTerminated': { $gte: new Date() } },
|
||||
{ 'purchased.plan.dateTerminated': { $exists: false } },
|
||||
{ 'purchased.plan.dateTerminated': { $eq: null } },
|
||||
],
|
||||
};
|
||||
|
||||
if (lastId) {
|
||||
query._id = {
|
||||
$gt: lastId,
|
||||
};
|
||||
}
|
||||
|
||||
dbUsers.find(query, {
|
||||
sort: {_id: 1},
|
||||
limit: 250,
|
||||
fields: [
|
||||
], // specify fields we are interested in to limit retrieved data (empty if we're not reading data):
|
||||
})
|
||||
.then(updateUsers)
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
return exiting(1, `ERROR! ${ err}`);
|
||||
});
|
||||
}
|
||||
|
||||
let progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
function updateUsers (users) {
|
||||
if (!users || users.length === 0) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
displayData();
|
||||
return;
|
||||
}
|
||||
|
||||
let userPromises = users.map(updateUser);
|
||||
let lastUser = users[users.length - 1];
|
||||
|
||||
return Promise.all(userPromises)
|
||||
.then(() => {
|
||||
processUsers(lastUser._id);
|
||||
});
|
||||
}
|
||||
|
||||
function updateUser (user) {
|
||||
count++;
|
||||
|
||||
const addToSet = {
|
||||
'purchased.plan.mysteryItems': {
|
||||
$each: MYSTERY_ITEMS,
|
||||
},
|
||||
};
|
||||
const push = {
|
||||
notifications: (new UserNotification({
|
||||
type: 'NEW_MYSTERY_ITEMS',
|
||||
data: {
|
||||
MYSTERY_ITEMS,
|
||||
},
|
||||
})).toJSON(),
|
||||
};
|
||||
|
||||
dbUsers.update({_id: user._id}, {$addToSet: addToSet, $push: push});
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count } ${ user._id}`);
|
||||
if (user._id === authorUuid) console.warn(`${authorName } processed`);
|
||||
}
|
||||
|
||||
function displayData () {
|
||||
console.warn(`\n${ count } users processed\n`);
|
||||
return exiting(0);
|
||||
}
|
||||
|
||||
function exiting (code, msg) {
|
||||
code = code || 0; // 0 = success
|
||||
if (code && !msg) {
|
||||
msg = 'ERROR!';
|
||||
}
|
||||
if (msg) {
|
||||
if (code) {
|
||||
console.error(msg);
|
||||
} else {
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
module.exports = processUsers;
|
||||
@@ -10,7 +10,7 @@ async function syncChallengeToMembers (challenges) {
|
||||
|
||||
const promises = [];
|
||||
users.forEach(user => {
|
||||
promises.push(challenge.syncToUser(user));
|
||||
promises.push(challenge.syncTasksToUser(user));
|
||||
promises.push(challenge.save());
|
||||
promises.push(user.save());
|
||||
});
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Award Onboarding Achievements for existing users
|
||||
*/
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import { model as User } from '../../website/server/models/user';
|
||||
|
||||
const MIGRATION_NAME = '20191218_onboarding_achievements';
|
||||
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
async function updateUser (user) {
|
||||
count += 1;
|
||||
|
||||
const set = {};
|
||||
|
||||
set.migration = MIGRATION_NAME;
|
||||
|
||||
const hasPet = Object.keys(user.items.pets).find(petKey => {
|
||||
const pet = user.items.pets[petKey];
|
||||
|
||||
if (pet >= 5) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
if (hasPet) {
|
||||
set['achievements.hatchedPet'] = true;
|
||||
}
|
||||
|
||||
const hasFedPet = Object.keys(user.items.pets).find(petKey => {
|
||||
const pet = user.items.pets[petKey];
|
||||
|
||||
if (pet > 5) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
if (hasFedPet) {
|
||||
set['achievements.fedPet'] = true;
|
||||
}
|
||||
|
||||
const hasGear = Object.keys(user.items.gear.owned).find(gearKey => {
|
||||
const gear = user.items.gear.owned[gearKey];
|
||||
|
||||
if (gear === true && gearKey.indexOf('_special_') === -1) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
if (hasGear) {
|
||||
set['achievements.purchasedEquipment'] = true;
|
||||
}
|
||||
|
||||
const hasTask = Object.keys(user.tasksOrder).find(tasksOrderType => {
|
||||
const order = user.tasksOrder[tasksOrderType];
|
||||
|
||||
if (order && order.length > 0) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
if (hasTask) {
|
||||
set['achievements.createdTask'] = true;
|
||||
}
|
||||
|
||||
const hasExperience = user.stats && user.stats.exp && user.stats.exp > 0;
|
||||
if (hasTask && hasExperience) {
|
||||
set['achievements.completedTask'] = true;
|
||||
}
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
return User.update({ _id: user._id }, { $set: set }).exec();
|
||||
}
|
||||
|
||||
module.exports = async function processUsers () { // eslint-disable-line import/no-commonjs
|
||||
const query = {
|
||||
migration: { $ne: MIGRATION_NAME },
|
||||
};
|
||||
|
||||
const fields = {
|
||||
_id: 1,
|
||||
stats: 1,
|
||||
items: 1,
|
||||
achievements: 1,
|
||||
tasksOrder: 1,
|
||||
};
|
||||
|
||||
while (true) { // eslint-disable-line no-constant-condition
|
||||
const users = await User // eslint-disable-line no-await-in-loop
|
||||
.find(query)
|
||||
.limit(100)
|
||||
.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
|
||||
}
|
||||
};
|
||||
@@ -16,7 +16,7 @@ async function updateUser (user) {
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
|
||||
sendTxn(
|
||||
await sendTxn(
|
||||
user,
|
||||
EMAIL_SLUG,
|
||||
[{ name: 'BASE_URL', content: BASE_URL }], // Add variables from template
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
/* eslint-disable no-console */
|
||||
import { model as User } from '../../website/server/models/user';
|
||||
import { model as UserNotification } from '../../website/server/models/userNotification';
|
||||
|
||||
const MIGRATION_NAME = 'mystery_items_201910';
|
||||
const MYSTERY_ITEMS = ['armor_mystery_201910', 'head_mystery_201910'];
|
||||
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
async function updateUser (user) {
|
||||
count += 1;
|
||||
|
||||
const addToSet = {
|
||||
'purchased.plan.mysteryItems': {
|
||||
$each: MYSTERY_ITEMS,
|
||||
},
|
||||
};
|
||||
const push = {
|
||||
notifications: (new UserNotification({
|
||||
type: 'NEW_MYSTERY_ITEMS',
|
||||
data: {
|
||||
MYSTERY_ITEMS,
|
||||
},
|
||||
})).toJSON(),
|
||||
};
|
||||
const set = {
|
||||
migration: MIGRATION_NAME,
|
||||
};
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
|
||||
return User.update({ _id: user._id }, { $set: set, $push: push, $addToSet: addToSet }).exec();
|
||||
}
|
||||
|
||||
export default async function processUsers () {
|
||||
const query = {
|
||||
migration: { $ne: MIGRATION_NAME },
|
||||
'purchased.plan.customerId': { $ne: null },
|
||||
$or: [
|
||||
{ 'purchased.plan.dateTerminated': { $gte: new Date() } },
|
||||
{ 'purchased.plan.dateTerminated': { $exists: false } },
|
||||
{ 'purchased.plan.dateTerminated': { $eq: null } },
|
||||
],
|
||||
};
|
||||
|
||||
const fields = {
|
||||
_id: 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,42 +1,42 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "4.120.0",
|
||||
"version": "4.127.1",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.6.4",
|
||||
"@babel/preset-env": "^7.6.3",
|
||||
"@babel/register": "^7.6.2",
|
||||
"@google-cloud/trace-agent": "^4.2.2",
|
||||
"@babel/core": "^7.7.5",
|
||||
"@babel/preset-env": "^7.7.6",
|
||||
"@babel/register": "^7.7.4",
|
||||
"@google-cloud/trace-agent": "^4.2.4",
|
||||
"@slack/client": "^3.8.1",
|
||||
"accepts": "^1.3.5",
|
||||
"amazon-payments": "^0.2.7",
|
||||
"amplitude": "^3.5.0",
|
||||
"apidoc": "^0.17.5",
|
||||
"apn": "^2.2.0",
|
||||
"aws-sdk": "^2.556.0",
|
||||
"bcrypt": "^3.0.6",
|
||||
"aws-sdk": "^2.590.0",
|
||||
"bcrypt": "^3.0.7",
|
||||
"body-parser": "^1.18.3",
|
||||
"compression": "^1.7.4",
|
||||
"cookie-session": "^1.3.3",
|
||||
"coupon-code": "^0.4.5",
|
||||
"csv-stringify": "^5.1.0",
|
||||
"csv-stringify": "^5.3.4",
|
||||
"cwait": "^1.1.1",
|
||||
"domain-middleware": "~0.1.0",
|
||||
"eslint": "^6.6.0",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-config-habitrpg": "^6.2.0",
|
||||
"eslint-plugin-mocha": "^5.0.0",
|
||||
"express": "^4.16.3",
|
||||
"express-basic-auth": "^1.1.5",
|
||||
"express-validator": "^5.2.0",
|
||||
"glob": "^7.1.5",
|
||||
"glob": "^7.1.6",
|
||||
"got": "^9.0.0",
|
||||
"gulp": "^4.0.0",
|
||||
"gulp-babel": "^8.0.0",
|
||||
"gulp-imagemin": "^6.1.1",
|
||||
"gulp-imagemin": "^6.2.0",
|
||||
"gulp-nodemon": "^2.4.1",
|
||||
"gulp.spritesmith": "^6.9.0",
|
||||
"habitica-markdown": "^1.3.0",
|
||||
"habitica-markdown": "^1.3.2",
|
||||
"helmet": "^3.21.2",
|
||||
"image-size": "^0.8.3",
|
||||
"in-app-purchase": "^1.11.3",
|
||||
@@ -46,12 +46,12 @@
|
||||
"method-override": "^3.0.0",
|
||||
"moment": "^2.24.0",
|
||||
"moment-recur": "^1.0.7",
|
||||
"mongoose": "^5.7.7",
|
||||
"mongoose": "^5.8.1",
|
||||
"morgan": "^1.7.0",
|
||||
"nconf": "^0.10.0",
|
||||
"node-gcm": "^1.0.2",
|
||||
"pageres": "^5.1.0",
|
||||
"passport": "^0.4.0",
|
||||
"passport": "^0.4.1",
|
||||
"passport-facebook": "^3.0.0",
|
||||
"passport-google-oauth2": "^0.2.0",
|
||||
"passport-google-oauth20": "1.0.0",
|
||||
@@ -59,10 +59,11 @@
|
||||
"paypal-rest-sdk": "^1.8.1",
|
||||
"ps-tree": "^1.0.0",
|
||||
"regenerator-runtime": "^0.13.3",
|
||||
"remove-markdown": "^0.3.0",
|
||||
"rimraf": "^3.0.0",
|
||||
"short-uuid": "^3.0.0",
|
||||
"stripe": "^7.10.0",
|
||||
"superagent": "^5.0.2",
|
||||
"stripe": "^7.14.0",
|
||||
"superagent": "^5.1.2",
|
||||
"universal-analytics": "^0.4.17",
|
||||
"useragent": "^2.1.9",
|
||||
"uuid": "^3.3.3",
|
||||
@@ -79,7 +80,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint --ext .js --fix . && cd website/client && npm run lint",
|
||||
"lint-no-fix": "eslint --ext .js . && cd website/client && npm run lint --no-fix",
|
||||
"lint-no-fix": "eslint --ext .js . && cd website/client && npm run lint-no-fix",
|
||||
"test": "npm run lint && gulp test && gulp apidoc",
|
||||
"test:build": "gulp test:prepare:build",
|
||||
"test:api-v3": "gulp test:api-v3",
|
||||
|
||||
@@ -24,7 +24,13 @@ async function deleteAmplitudeData (userId, email) {
|
||||
console.log(err.response.data);
|
||||
});
|
||||
|
||||
if (response) console.log(`${response.status} ${response.statusText}`);
|
||||
if (response) {
|
||||
if (response.status === 200) {
|
||||
console.log(`${userId} (${email}) Amplitude deletion request OK.`);
|
||||
} else {
|
||||
console.log(`${userId} (${email}) Amplitude response: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteHabiticaData (user, email) {
|
||||
@@ -54,39 +60,46 @@ async function deleteHabiticaData (user, email) {
|
||||
});
|
||||
|
||||
if (response) {
|
||||
console.log(`${response.status} ${response.statusText}`);
|
||||
if (response.status === 200) console.log(`${user._id} (${email}) removed. Last login: ${user.auth.timestamps.loggedin}`);
|
||||
if (response.status === 200) {
|
||||
console.log(`${user._id} (${email}) removed from Habitica. Last login: ${user.auth.timestamps.loggedin}`);
|
||||
} else {
|
||||
console.log(`${user._id} (${email}) Habitica response: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function processEmailAddress (email) {
|
||||
const emailRegex = new RegExp(`^${email}$`, 'i');
|
||||
const users = await User.find({
|
||||
$or: [
|
||||
{ 'auth.local.email': emailRegex },
|
||||
{ 'auth.facebook.emails.value': emailRegex },
|
||||
{ 'auth.google.emails.value': emailRegex },
|
||||
],
|
||||
},
|
||||
{
|
||||
_id: 1,
|
||||
apiToken: 1,
|
||||
auth: 1,
|
||||
}).exec();
|
||||
const localUsers = await User.find(
|
||||
{ 'auth.local.email': emailRegex },
|
||||
{ _id: 1, apiToken: 1, auth: 1 },
|
||||
).exec();
|
||||
|
||||
const socialUsers = await User.find(
|
||||
{
|
||||
$or: [
|
||||
{ 'auth.facebook.emails.value': email },
|
||||
{ 'auth.google.emails.value': email },
|
||||
],
|
||||
},
|
||||
{ _id: 1, apiToken: 1, auth: 1 },
|
||||
).collation(
|
||||
{ locale: 'en', strength: 1 },
|
||||
).exec();
|
||||
|
||||
const users = localUsers.concat(socialUsers);
|
||||
|
||||
if (users.length < 1) {
|
||||
console.log(`No users found with email address ${email}`);
|
||||
} else {
|
||||
Promise.all(users.map(user => (async () => {
|
||||
await deleteAmplitudeData(user._id, email); // eslint-disable-line no-await-in-loop
|
||||
await deleteHabiticaData(user, email); // eslint-disable-line no-await-in-loop
|
||||
})()));
|
||||
return console.log(`No users found with email address ${email}`);
|
||||
}
|
||||
|
||||
return Promise.all(users.map(user => (async () => {
|
||||
await deleteAmplitudeData(user._id, email); // eslint-disable-line no-await-in-loop
|
||||
await deleteHabiticaData(user, email); // eslint-disable-line no-await-in-loop
|
||||
})()));
|
||||
}
|
||||
|
||||
function deleteUserData (emails) {
|
||||
export default function deleteUserData (emails) {
|
||||
const emailPromises = emails.map(processEmailAddress);
|
||||
return Promise.all(emailPromises);
|
||||
}
|
||||
|
||||
module.exports = deleteUserData;
|
||||
|
||||
@@ -88,6 +88,28 @@ describe('cron', () => {
|
||||
user.purchased.plan.dateUpdated = moment().subtract(1, 'months').toDate();
|
||||
});
|
||||
|
||||
it('awards current mystery items to subscriber', () => {
|
||||
user.purchased.plan.dateUpdated = new Date('2018-12-11');
|
||||
clock = sinon.useFakeTimers(new Date('2019-01-29'));
|
||||
cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.mysteryItems.length).to.eql(2);
|
||||
const filteredNotifications = user.notifications.filter(n => n.type === 'NEW_MYSTERY_ITEMS');
|
||||
expect(filteredNotifications.length).to.equal(1);
|
||||
});
|
||||
|
||||
it('awards multiple mystery item sets if user skipped months between logins', () => {
|
||||
user.purchased.plan.dateUpdated = new Date('2018-11-11');
|
||||
clock = sinon.useFakeTimers(new Date('2019-01-29'));
|
||||
cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.mysteryItems.length).to.eql(4);
|
||||
const filteredNotifications = user.notifications.filter(n => n.type === 'NEW_MYSTERY_ITEMS');
|
||||
expect(filteredNotifications.length).to.equal(1);
|
||||
});
|
||||
|
||||
it('resets plan.gemsBought on a new month', () => {
|
||||
user.purchased.plan.gemsBought = 10;
|
||||
cron({
|
||||
|
||||
@@ -11,6 +11,7 @@ import { model as User } from '../../../../../../website/server/models/user';
|
||||
import { model as Group } from '../../../../../../website/server/models/group';
|
||||
import {
|
||||
generateGroup,
|
||||
sleep,
|
||||
} from '../../../../../helpers/api-unit.helper';
|
||||
|
||||
describe('Purchasing a group plan for group', () => {
|
||||
@@ -307,6 +308,7 @@ describe('Purchasing a group plan for group', () => {
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
await sleep(0.5);
|
||||
|
||||
expect(sender.sendTxn).to.have.callCount(4);
|
||||
expect(sender.sendTxn.args[0][0]._id).to.equal(TECH_ASSISTANCE_EMAIL);
|
||||
@@ -340,6 +342,7 @@ describe('Purchasing a group plan for group', () => {
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
await sleep(0.5);
|
||||
|
||||
expect(sender.sendTxn).to.have.callCount(4);
|
||||
expect(sender.sendTxn.args[0][0]._id).to.equal(TECH_ASSISTANCE_EMAIL);
|
||||
|
||||
@@ -211,7 +211,7 @@ describe('payments/index', () => {
|
||||
await api.createSubscription(data);
|
||||
const msg = '`Hello recipient, sender has sent you 3 months of subscription!`';
|
||||
|
||||
expect(user.sendMessage).to.be.calledOnce;
|
||||
expect(user.sendMessage).to.be.calledTwice;
|
||||
expect(user.sendMessage).to.be.calledWith(
|
||||
recipient,
|
||||
{ receiverMsg: msg, senderMsg: msg, save: false },
|
||||
@@ -252,6 +252,77 @@ describe('payments/index', () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
context('Winter 2019-20 Gift-1-Get-1 Promotion', async () => {
|
||||
it('creates a gift subscription for purchaser and recipient if none exist', async () => {
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.items.pets['Jackalope-RoyalPurple']).to.eql(5);
|
||||
expect(user.purchased.plan.customerId).to.eql('Gift');
|
||||
expect(user.purchased.plan.dateTerminated).to.exist;
|
||||
expect(user.purchased.plan.dateUpdated).to.exist;
|
||||
expect(user.purchased.plan.dateCreated).to.exist;
|
||||
|
||||
expect(recipient.items.pets['Jackalope-RoyalPurple']).to.eql(5);
|
||||
expect(recipient.purchased.plan.customerId).to.eql('Gift');
|
||||
expect(recipient.purchased.plan.dateTerminated).to.exist;
|
||||
expect(recipient.purchased.plan.dateUpdated).to.exist;
|
||||
expect(recipient.purchased.plan.dateCreated).to.exist;
|
||||
});
|
||||
|
||||
it('adds extraMonths to existing subscription for purchaser and creates a gift subscription for recipient without sub', async () => {
|
||||
user.purchased.plan = plan;
|
||||
|
||||
expect(user.purchased.plan.extraMonths).to.eql(0);
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.extraMonths).to.eql(3);
|
||||
|
||||
expect(recipient.items.pets['Jackalope-RoyalPurple']).to.eql(5);
|
||||
expect(recipient.purchased.plan.customerId).to.eql('Gift');
|
||||
expect(recipient.purchased.plan.dateTerminated).to.exist;
|
||||
expect(recipient.purchased.plan.dateUpdated).to.exist;
|
||||
expect(recipient.purchased.plan.dateCreated).to.exist;
|
||||
});
|
||||
|
||||
it('adds extraMonths to existing subscription for recipient and creates a gift subscription for purchaser without sub', async () => {
|
||||
recipient.purchased.plan = plan;
|
||||
|
||||
expect(recipient.purchased.plan.extraMonths).to.eql(0);
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.extraMonths).to.eql(3);
|
||||
|
||||
expect(user.items.pets['Jackalope-RoyalPurple']).to.eql(5);
|
||||
expect(user.purchased.plan.customerId).to.eql('Gift');
|
||||
expect(user.purchased.plan.dateTerminated).to.exist;
|
||||
expect(user.purchased.plan.dateUpdated).to.exist;
|
||||
expect(user.purchased.plan.dateCreated).to.exist;
|
||||
});
|
||||
|
||||
it('adds extraMonths to existing subscriptions for purchaser and recipient', async () => {
|
||||
user.purchased.plan = plan;
|
||||
recipient.purchased.plan = plan;
|
||||
|
||||
expect(user.purchased.plan.extraMonths).to.eql(0);
|
||||
expect(recipient.purchased.plan.extraMonths).to.eql(0);
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.extraMonths).to.eql(3);
|
||||
expect(recipient.purchased.plan.extraMonths).to.eql(3);
|
||||
});
|
||||
|
||||
it('sends a private message about the promotion', async () => {
|
||||
await api.createSubscription(data);
|
||||
const msg = '`Hello sender, you received 3 months of subscription as part of our holiday gift-giving promotion!`';
|
||||
|
||||
expect(user.sendMessage).to.be.calledTwice;
|
||||
expect(user.sendMessage).to.be.calledWith(user, { senderMsg: msg });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Purchasing a subscription for self', () => {
|
||||
@@ -439,31 +510,6 @@ describe('payments/index', () => {
|
||||
fakeClock.restore();
|
||||
});
|
||||
|
||||
it('does not awards mystery items when not within the timeframe for a mystery item', async () => {
|
||||
const noMysteryItemTimeframe = 1462183920000; // May 2nd 2016
|
||||
const fakeClock = sinon.useFakeTimers(noMysteryItemTimeframe);
|
||||
data = { paymentMethod: 'PaymentMethod', user, sub: { key: 'basic_3mo' } };
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.mysteryItems).to.have.a.lengthOf(0);
|
||||
|
||||
fakeClock.restore();
|
||||
});
|
||||
|
||||
it('does not add a notification for mystery items if none was awarded', async () => {
|
||||
const noMysteryItemTimeframe = 1462183920000; // May 2nd 2016
|
||||
const fakeClock = sinon.useFakeTimers(noMysteryItemTimeframe);
|
||||
data = { paymentMethod: 'PaymentMethod', user, sub: { key: 'basic_3mo' } };
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.mysteryItems).to.have.a.lengthOf(0);
|
||||
expect(user.notifications.find(n => n.type === 'NEW_MYSTERY_ITEMS')).to.be.undefined;
|
||||
|
||||
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);
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
moveTask,
|
||||
} from '../../../../website/server/libs/taskManager';
|
||||
import i18n from '../../../../website/common/script/i18n';
|
||||
import shared from '../../../../website/common/script';
|
||||
import {
|
||||
generateUser,
|
||||
generateGroup,
|
||||
@@ -58,6 +59,51 @@ describe('taskManager', () => {
|
||||
expect(newTask.createdAt).to.exist;
|
||||
});
|
||||
|
||||
describe('onboarding', () => {
|
||||
beforeEach(() => {
|
||||
user.addAchievement = sinon.spy();
|
||||
sinon.stub(shared.onboarding, 'checkOnboardingStatus');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
shared.onboarding.checkOnboardingStatus.restore();
|
||||
});
|
||||
|
||||
it('adds the onboarding achievement to the user and checks the onboarding status', async () => {
|
||||
req.body = testHabit;
|
||||
res.t = i18n.t;
|
||||
user.flags.welcomed = true;
|
||||
|
||||
await createTasks(req, res, { user });
|
||||
|
||||
expect(user.addAchievement).to.be.calledOnce;
|
||||
expect(user.addAchievement).to.be.calledWith('createdTask');
|
||||
|
||||
expect(shared.onboarding.checkOnboardingStatus).to.be.calledOnce;
|
||||
expect(shared.onboarding.checkOnboardingStatus).to.be.calledWith(user);
|
||||
});
|
||||
|
||||
it('does not add the onboarding achievement to the user if flags.welcomed is false', async () => {
|
||||
req.body = testHabit;
|
||||
res.t = i18n.t;
|
||||
user.flags.welcomed = false;
|
||||
|
||||
await createTasks(req, res, { user });
|
||||
|
||||
expect(user.addAchievement).to.not.be.called;
|
||||
});
|
||||
|
||||
it('does not add the onboarding achievement to the user if it\'s already been awarded', async () => {
|
||||
req.body = testHabit;
|
||||
res.t = i18n.t;
|
||||
user.achievements.createdTask = true;
|
||||
|
||||
await createTasks(req, res, { user });
|
||||
|
||||
expect(user.addAchievement).to.not.be.called;
|
||||
});
|
||||
});
|
||||
|
||||
it('gets user tasks', async () => {
|
||||
req.body = testHabit;
|
||||
res.t = i18n.t;
|
||||
|
||||
@@ -90,7 +90,37 @@ describe('Challenge Model', () => {
|
||||
expect(syncedTask.tags[0]).to.eql(challenge._id);
|
||||
});
|
||||
|
||||
it('syncs a challenge to a user', async () => {
|
||||
it('adds a challenge to a user', async () => {
|
||||
const newMember = new User({
|
||||
guilds: [guild._id],
|
||||
});
|
||||
await newMember.save();
|
||||
|
||||
const addedSuccessfully = await challenge.addToUser(newMember);
|
||||
|
||||
const updatedNewMember = await User.findById(newMember._id);
|
||||
|
||||
expect(addedSuccessfully).to.eql(true);
|
||||
expect(updatedNewMember.challenges).to.contain(challenge._id);
|
||||
});
|
||||
|
||||
it('does not add a challenge to a user that already in the challenge', async () => {
|
||||
const newMember = new User({
|
||||
guilds: [guild._id],
|
||||
challenges: [challenge._id],
|
||||
});
|
||||
await newMember.save();
|
||||
|
||||
const addedSuccessfully = await challenge.addToUser(newMember);
|
||||
|
||||
const updatedNewMember = await User.findById(newMember._id);
|
||||
|
||||
expect(addedSuccessfully).to.eql(false);
|
||||
expect(updatedNewMember.challenges).to.contain(challenge._id);
|
||||
expect(updatedNewMember.challenges.length).to.eql(1);
|
||||
});
|
||||
|
||||
it('syncs challenge tasks to a user', async () => {
|
||||
await challenge.addTasks([task]);
|
||||
|
||||
const newMember = new User({
|
||||
@@ -98,7 +128,7 @@ describe('Challenge Model', () => {
|
||||
});
|
||||
await newMember.save();
|
||||
|
||||
await challenge.syncToUser(newMember);
|
||||
await challenge.syncTasksToUser(newMember);
|
||||
|
||||
const updatedNewMember = await User.findById(newMember._id);
|
||||
const updatedNewMemberTasks = await Tasks.Task.find({ _id: { $in: updatedNewMember.tasksOrder[`${taskType}s`] } });
|
||||
@@ -110,14 +140,13 @@ describe('Challenge Model', () => {
|
||||
),
|
||||
);
|
||||
|
||||
expect(updatedNewMember.challenges).to.contain(challenge._id);
|
||||
expect(updatedNewMember.tags[7].id).to.equal(challenge._id);
|
||||
expect(updatedNewMember.tags[7].name).to.equal(challenge.shortName);
|
||||
expect(syncedTask).to.exist;
|
||||
expect(syncedTask.attribute).to.eql('str');
|
||||
});
|
||||
|
||||
it('syncs a challenge to a user with the existing task', async () => {
|
||||
it('syncs challenge tasks to a user with the existing task', async () => {
|
||||
await challenge.addTasks([task]);
|
||||
|
||||
let updatedLeader = await User.findOne({ _id: leader._id });
|
||||
@@ -134,7 +163,7 @@ describe('Challenge Model', () => {
|
||||
task.text = newTitle;
|
||||
task.attribute = 'int';
|
||||
await task.save();
|
||||
await challenge.syncToUser(leader);
|
||||
await challenge.syncTasksToUser(leader);
|
||||
|
||||
updatedLeader = await User.findOne({ _id: leader._id });
|
||||
updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } });
|
||||
|
||||
@@ -1317,7 +1317,7 @@ describe('Group Model', () => {
|
||||
|
||||
it('formats message', () => {
|
||||
const chatMessage = party.sendChat({
|
||||
message: 'a new message',
|
||||
message: 'a _new_ message with *markdown*',
|
||||
user: {
|
||||
_id: 'user-id',
|
||||
profile: { name: 'user name' },
|
||||
@@ -1336,7 +1336,8 @@ describe('Group Model', () => {
|
||||
|
||||
const chat = chatMessage;
|
||||
|
||||
expect(chat.text).to.eql('a new message');
|
||||
expect(chat.text).to.eql('a _new_ message with *markdown*');
|
||||
expect(chat.unformattedText).to.eql('a new message with markdown');
|
||||
expect(validator.isUUID(chat.id)).to.eql(true);
|
||||
expect(chat.timestamp).to.be.a('date');
|
||||
expect(chat.likes).to.eql({});
|
||||
@@ -1878,6 +1879,8 @@ describe('Group Model', () => {
|
||||
await questLeader.save();
|
||||
await party.finishQuest(quest);
|
||||
|
||||
await sleep(0.5);
|
||||
|
||||
const [
|
||||
updatedLeader,
|
||||
updatedParticipatingMember,
|
||||
|
||||
@@ -84,6 +84,103 @@ describe('User Model', () => {
|
||||
expect(userToJSON.stats.toNextLevel).to.equal(common.tnl(user.stats.lvl));
|
||||
});
|
||||
|
||||
context('achievements', () => {
|
||||
it('can add an achievement', () => {
|
||||
const user = new User();
|
||||
const originalUserToJSON = user.toJSON({ minimize: false });
|
||||
expect(originalUserToJSON.achievements.createdTask).to.not.eql(true);
|
||||
const notificationsN = originalUserToJSON.notifications.length;
|
||||
|
||||
user.addAchievement('createdTask');
|
||||
|
||||
const userToJSON = user.toJSON();
|
||||
expect(user.notifications.length).to.equal(notificationsN + 1);
|
||||
expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type', 'seen']);
|
||||
expect(userToJSON.notifications[0].type).to.equal('ACHIEVEMENT');
|
||||
expect(userToJSON.notifications[0].data).to.eql({
|
||||
achievement: 'createdTask',
|
||||
});
|
||||
expect(userToJSON.notifications[0].seen).to.eql(false);
|
||||
|
||||
expect(userToJSON.achievements.createdTask).to.eql(true);
|
||||
});
|
||||
|
||||
it('throws an error if the achievement is not valid', () => {
|
||||
const user = new User();
|
||||
expect(() => user.addAchievement('notAnAchievement')).to.throw;
|
||||
});
|
||||
|
||||
context('static push method', () => {
|
||||
it('throws an error if the achievement is not valid', async () => {
|
||||
const user = new User();
|
||||
await user.save();
|
||||
|
||||
await expect(User.addAchievementUpdate({ _id: user._id }, 'notAnAchievement'))
|
||||
.to.eventually.be.rejected;
|
||||
|
||||
expect(() => user.addAchievement('notAnAchievement')).to.throw;
|
||||
});
|
||||
|
||||
it('adds an achievement for a single member via static method', async () => {
|
||||
let user = new User();
|
||||
await user.save();
|
||||
|
||||
const originalUserToJSON = user.toJSON({ minimize: false });
|
||||
expect(originalUserToJSON.achievements.createdTask).to.not.eql(true);
|
||||
const notificationsN = originalUserToJSON.notifications.length;
|
||||
|
||||
await User.addAchievementUpdate({ _id: user._id }, 'createdTask');
|
||||
|
||||
user = await User.findOne({ _id: user._id }).exec();
|
||||
|
||||
const userToJSON = user.toJSON();
|
||||
expect(user.notifications.length).to.equal(notificationsN + 1);
|
||||
expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type', 'seen']);
|
||||
expect(userToJSON.notifications[0].type).to.equal('ACHIEVEMENT');
|
||||
expect(userToJSON.notifications[0].data).to.eql({
|
||||
achievement: 'createdTask',
|
||||
});
|
||||
expect(userToJSON.notifications[0].seen).to.eql(false);
|
||||
|
||||
expect(userToJSON.achievements.createdTask).to.eql(true);
|
||||
});
|
||||
|
||||
it('adds an achievement for all given users via static method', async () => {
|
||||
let user = new User();
|
||||
const otherUser = new User();
|
||||
await Promise.all([user.save(), otherUser.save()]);
|
||||
|
||||
await User.addAchievementUpdate({ _id: { $in: [user._id, otherUser._id] } }, 'createdTask');
|
||||
|
||||
user = await User.findOne({ _id: user._id }).exec();
|
||||
|
||||
let userToJSON = user.toJSON();
|
||||
expect(user.notifications.length).to.equal(1);
|
||||
expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type', 'seen']);
|
||||
expect(userToJSON.notifications[0].type).to.equal('ACHIEVEMENT');
|
||||
expect(userToJSON.notifications[0].data).to.eql({
|
||||
achievement: 'createdTask',
|
||||
});
|
||||
expect(userToJSON.notifications[0].seen).to.eql(false);
|
||||
|
||||
expect(userToJSON.achievements.createdTask).to.eql(true);
|
||||
|
||||
user = await User.findOne({ _id: otherUser._id }).exec();
|
||||
|
||||
userToJSON = user.toJSON();
|
||||
expect(user.notifications.length).to.equal(1);
|
||||
expect(userToJSON.notifications[0]).to.have.all.keys(['data', 'id', 'type', 'seen']);
|
||||
expect(userToJSON.notifications[0].type).to.equal('ACHIEVEMENT');
|
||||
expect(userToJSON.notifications[0].data).to.eql({
|
||||
achievement: 'createdTask',
|
||||
});
|
||||
expect(userToJSON.notifications[0].seen).to.eql(false);
|
||||
|
||||
expect(userToJSON.achievements.createdTask).to.eql(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('notifications', () => {
|
||||
it('can add notifications without data', () => {
|
||||
const user = new User();
|
||||
|
||||
@@ -206,16 +206,16 @@ describe('GET /challenges/:challengeId/members', () => {
|
||||
for (let i = 0; i < 3; i += 1) {
|
||||
usersToGenerate.push(generateUser({
|
||||
challenges: [challenge._id],
|
||||
'profile.name': `${i}profilename`,
|
||||
'auth.local.username': `${i}username`,
|
||||
}));
|
||||
}
|
||||
const generatedUsers = await Promise.all(usersToGenerate);
|
||||
const profileNames = generatedUsers.map(generatedUser => generatedUser.profile.name);
|
||||
const usernames = generatedUsers.map(generatedUser => generatedUser.auth.local.username);
|
||||
|
||||
const firstProfileName = profileNames[0];
|
||||
const nameToSearch = firstProfileName.substring(0, 4);
|
||||
const firstUsername = usernames[0];
|
||||
const nameToSearch = firstUsername.substring(0, 4);
|
||||
|
||||
const response = await user.get(`/challenges/${challenge._id}/members?search=${nameToSearch}`);
|
||||
expect(response[0].profile.name).to.eql(firstProfileName);
|
||||
expect(response[0].auth.local.username).to.eql(firstUsername);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,8 @@ import {
|
||||
|
||||
describe('POST /members/send-private-message', () => {
|
||||
let userToSendMessage;
|
||||
const messageToSend = 'Test Private Message';
|
||||
const messageToSend = 'Test *Private* Message';
|
||||
const unformattedMessage = 'Test Private Message';
|
||||
|
||||
beforeEach(async () => {
|
||||
userToSendMessage = await generateUser();
|
||||
@@ -110,7 +111,9 @@ describe('POST /members/send-private-message', () => {
|
||||
|
||||
const sendersMessageInReceiversInbox = _.find(
|
||||
updatedReceiver.inbox.messages,
|
||||
message => message.uuid === userToSendMessage._id && message.text === messageToSend,
|
||||
message => message.uuid === userToSendMessage._id
|
||||
&& message.text === messageToSend
|
||||
&& message.unformattedText === unformattedMessage,
|
||||
);
|
||||
|
||||
const sendersMessageInSendersInbox = _.find(
|
||||
|
||||
@@ -15,7 +15,7 @@ describe('GET /user/toggle-pinned-item', () => {
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('cannotUnpinArmoirPotion'),
|
||||
message: t('cannotUnpinItem'),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -189,6 +189,28 @@ describe('POST /user/auth/reset-password-set-new-one', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the error page if the password is too short', async () => {
|
||||
const user = await generateUser();
|
||||
|
||||
const code = encrypt(JSON.stringify({
|
||||
userId: user._id,
|
||||
expiresAt: moment().add({ days: 1 }),
|
||||
}));
|
||||
await user.update({
|
||||
'auth.local.passwordResetCode': code,
|
||||
});
|
||||
|
||||
await expect(api.post(`${endpoint}`, {
|
||||
newPassword: 'short',
|
||||
confirmPassword: 'short',
|
||||
code,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the success page and save the user', async () => {
|
||||
const user = await generateUser();
|
||||
|
||||
|
||||
@@ -326,6 +326,24 @@ describe('POST /user/auth/local/register', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('requires minimum length for the password', async () => {
|
||||
const username = generateRandomUserName();
|
||||
const email = `${username}@example.com`;
|
||||
const password = '1234567';
|
||||
const confirmPassword = '1234567';
|
||||
|
||||
await expect(api.post('/user/auth/local/register', {
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
confirmPassword,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('requires a username', async () => {
|
||||
const email = `${generateRandomUserName()}@example.com`;
|
||||
const password = 'password';
|
||||
|
||||
@@ -82,6 +82,20 @@ describe('PUT /user/auth/update-password', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error when newPassword is too short', async () => {
|
||||
const body = {
|
||||
password,
|
||||
newPassword: '1234567',
|
||||
confirmPassword: '1234567',
|
||||
};
|
||||
|
||||
await expect(user.put(ENDPOINT, body)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error when confirmPassword is missing', async () => {
|
||||
const body = {
|
||||
password,
|
||||
|
||||
@@ -63,6 +63,26 @@ describe('PUT /user/webhook/:id', () => {
|
||||
expect(webhook.options).to.eql(options);
|
||||
});
|
||||
|
||||
it('updates a webhook with empty label', async () => {
|
||||
const url = 'http://a-new-url.com';
|
||||
const type = 'groupChatReceived';
|
||||
const label = '';
|
||||
const options = { groupId: generateUUID() };
|
||||
|
||||
await user.put(`/user/webhook/${webhookToUpdate.id}`, {
|
||||
url, type, options, label,
|
||||
});
|
||||
|
||||
await user.sync();
|
||||
|
||||
const webhook = user.webhooks.find(hook => webhookToUpdate.id === hook.id);
|
||||
|
||||
expect(webhook.url).to.equal(url);
|
||||
expect(webhook.label).to.equal(label);
|
||||
expect(webhook.type).to.equal(type);
|
||||
expect(webhook.options).to.eql(options);
|
||||
});
|
||||
|
||||
it('returns the updated webhook', async () => {
|
||||
const url = 'http://a-new-url.com';
|
||||
const type = 'groupChatReceived';
|
||||
|
||||
@@ -239,6 +239,47 @@ describe('achievements', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('unearned onboarding achievements', () => {
|
||||
const user = generateUser();
|
||||
const onboardingAchievs = shared
|
||||
.achievements.getAchievementsForProfile(user).onboarding.achievements;
|
||||
|
||||
it('created task achievement exists with no count', () => {
|
||||
const { createdTask } = onboardingAchievs;
|
||||
|
||||
expect(createdTask).to.exist;
|
||||
expect(createdTask.optionalCount).to.be.undefined;
|
||||
});
|
||||
|
||||
it('completed task achievement exists with no count', () => {
|
||||
const { completedTask } = onboardingAchievs;
|
||||
|
||||
expect(completedTask).to.exist;
|
||||
expect(completedTask.optionalCount).to.be.undefined;
|
||||
});
|
||||
|
||||
it('hatched pet achievement exists with no count', () => {
|
||||
const { hatchedPet } = onboardingAchievs;
|
||||
|
||||
expect(hatchedPet).to.exist;
|
||||
expect(hatchedPet.optionalCount).to.be.undefined;
|
||||
});
|
||||
|
||||
it('fed pet achievement exists with no count', () => {
|
||||
const { fedPet } = onboardingAchievs;
|
||||
|
||||
expect(fedPet).to.exist;
|
||||
expect(fedPet.optionalCount).to.be.undefined;
|
||||
});
|
||||
|
||||
it('purchased equipment achievement exists with no count', () => {
|
||||
const { purchasedEquipment } = onboardingAchievs;
|
||||
|
||||
expect(purchasedEquipment).to.exist;
|
||||
expect(purchasedEquipment.optionalCount).to.be.undefined;
|
||||
});
|
||||
});
|
||||
|
||||
describe('earned seasonal achievements', () => {
|
||||
const user = generateUser();
|
||||
const quests = ['dilatory', 'stressbeast', 'burnout', 'bewilder'];
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../helpers/common.helper';
|
||||
|
||||
import getDebuffPotionItems from '../../../website/common/script/libs/getDebuffPotionItems';
|
||||
import { TRANSFORMATION_DEBUFFS_LIST } from '../../../website/common/script/constants';
|
||||
|
||||
describe('getDebuffPotionItems', () => {
|
||||
let user;
|
||||
|
||||
beforeEach(() => {
|
||||
user = generateUser();
|
||||
});
|
||||
|
||||
for (const key of Object.keys(TRANSFORMATION_DEBUFFS_LIST)) {
|
||||
const debuff = TRANSFORMATION_DEBUFFS_LIST[key];
|
||||
// Here we itterate whole object to dynamicaly create test suites as
|
||||
// it described in dock of mocha
|
||||
// https://mochajs.org/#dynamically-generating-tests
|
||||
// That's why we have eslint-disable here
|
||||
// eslint-disable-next-line no-loop-func
|
||||
it(`Should return the ${debuff} on ${key} buff`, () => {
|
||||
user.stats.buffs[key] = true;
|
||||
|
||||
const result = getDebuffPotionItems(user);
|
||||
|
||||
expect(result).to.be.an('array').that.deep
|
||||
.includes({ path: `spells.special.${debuff}`, type: 'debuffPotion' });
|
||||
});
|
||||
}
|
||||
|
||||
it('Should return all debuff potions for all buffs', () => {
|
||||
user.stats.buffs.seafoam = true;
|
||||
user.stats.buffs.spookySparkles = true;
|
||||
user.stats.buffs.snowball = true;
|
||||
user.stats.buffs.shinySeed = true;
|
||||
|
||||
|
||||
const result = getDebuffPotionItems(user);
|
||||
|
||||
expect(result).to.be.an('array').that.deep.include.members([
|
||||
{ path: 'spells.special.sand', type: 'debuffPotion' },
|
||||
{ path: 'spells.special.petalFreePotion', type: 'debuffPotion' },
|
||||
{ path: 'spells.special.salt', type: 'debuffPotion' },
|
||||
{ path: 'spells.special.opaquePotion', type: 'debuffPotion' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,100 @@
|
||||
import moment from 'moment';
|
||||
import {
|
||||
hasActiveOnboarding,
|
||||
hasCompletedOnboarding,
|
||||
onOnboardingComplete,
|
||||
checkOnboardingStatus,
|
||||
} from '../../../website/common/script/libs/onboarding';
|
||||
import { generateUser } from '../../helpers/common.helper';
|
||||
|
||||
describe('onboarding', () => {
|
||||
let user;
|
||||
|
||||
beforeEach(() => {
|
||||
user = generateUser();
|
||||
user.addNotification = sinon.spy();
|
||||
// Make sure the onboarding is active
|
||||
user.auth.timestamps.created = moment('2019-12-20').toDate();
|
||||
});
|
||||
|
||||
describe('hasActiveOnboarding', () => {
|
||||
// The value of BEGIN DATE is available in common/script/libs/onboarding
|
||||
|
||||
it('returns true if the account is created after BEGIN_DATE', () => {
|
||||
expect(hasActiveOnboarding(user)).to.eql(true);
|
||||
});
|
||||
|
||||
it('returns false if the account is created before BEGIN_DATE', () => {
|
||||
user.auth.timestamps.created = moment('2019-12-01').toDate();
|
||||
expect(hasActiveOnboarding(user)).to.eql(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasCompletedOnboarding', () => {
|
||||
it('returns false if no achievement has been awarded', () => {
|
||||
const result = hasCompletedOnboarding(user);
|
||||
expect(result).to.eql(false);
|
||||
});
|
||||
|
||||
it('returns false if not all achievements have been awarded', () => {
|
||||
user.achievements.completedTask = true;
|
||||
const result = hasCompletedOnboarding(user);
|
||||
expect(result).to.eql(false);
|
||||
});
|
||||
|
||||
it('returns true if all achievements have been awarded', () => {
|
||||
user.achievements.createdTask = true;
|
||||
user.achievements.completedTask = true;
|
||||
user.achievements.hatchedPet = true;
|
||||
user.achievements.fedPet = true;
|
||||
user.achievements.purchasedEquipment = true;
|
||||
|
||||
const result = hasCompletedOnboarding(user);
|
||||
expect(result).to.eql(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onOnboardingComplete', () => {
|
||||
it('awards prizes', () => {
|
||||
const { gp } = user.stats;
|
||||
onOnboardingComplete(user);
|
||||
expect(user.stats.gp).to.eql(gp + 100);
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkOnboardingStatus', () => {
|
||||
it('does nothing if onboarding is not active', () => {
|
||||
const { gp } = user.stats;
|
||||
user.auth.timestamps.created = moment('2019-12-01').toDate();
|
||||
|
||||
checkOnboardingStatus(user);
|
||||
expect(user.addNotification).to.not.be.called;
|
||||
|
||||
expect(user.stats.gp).to.eql(gp);
|
||||
});
|
||||
|
||||
it('does nothing if onboarding is not complete', () => {
|
||||
const { gp } = user.stats;
|
||||
|
||||
checkOnboardingStatus(user);
|
||||
expect(user.addNotification).to.not.be.called;
|
||||
|
||||
expect(user.stats.gp).to.eql(gp);
|
||||
});
|
||||
|
||||
it('awards prize and add notification when onboarding is complete', () => {
|
||||
user.achievements.createdTask = true;
|
||||
user.achievements.completedTask = true;
|
||||
user.achievements.hatchedPet = true;
|
||||
user.achievements.fedPet = true;
|
||||
user.achievements.purchasedEquipment = true;
|
||||
const { gp } = user.stats;
|
||||
|
||||
checkOnboardingStatus(user);
|
||||
expect(user.addNotification).to.be.calledOnce;
|
||||
expect(user.addNotification).to.be.calledWith('ONBOARDING_COMPLETE');
|
||||
|
||||
expect(user.stats.gp).to.eql(gp + 100);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../helpers/common.helper';
|
||||
|
||||
import setDebuffPotionItems from '../../../website/common/script/libs/setDebuffPotionItems';
|
||||
|
||||
describe('setDebuffPotionItems', () => {
|
||||
let user;
|
||||
|
||||
beforeEach(() => {
|
||||
user = generateUser();
|
||||
});
|
||||
|
||||
it('Should push the debuff item to pinned items of user', () => {
|
||||
user.stats.buffs.spookySparkles = true;
|
||||
const previousPinnedItemsLength = user.pinnedItems.length;
|
||||
|
||||
const result = setDebuffPotionItems(user);
|
||||
|
||||
expect(result.pinnedItems.length).to.be.greaterThan(previousPinnedItemsLength);
|
||||
});
|
||||
|
||||
it('Shouldn\'t create duplicate of already added debuff potion', () => {
|
||||
user.stats.buffs.spookySparkles = true;
|
||||
|
||||
const firstSetResult = [...setDebuffPotionItems(user).pinnedItems];
|
||||
const secondSetResult = [...setDebuffPotionItems(user).pinnedItems];
|
||||
|
||||
|
||||
expect(firstSetResult).to.be.deep.equal(secondSetResult);
|
||||
});
|
||||
|
||||
it('Should remove all debuff items from pinnedItems of the user if user have no buffs', () => {
|
||||
user.stats.buffs.seafoam = true;
|
||||
user.stats.buffs.spookySparkles = true;
|
||||
user.stats.buffs.snowball = true;
|
||||
user.stats.buffs.shinySeed = true;
|
||||
|
||||
const firstSetResult = [...setDebuffPotionItems(user).pinnedItems];
|
||||
|
||||
expect(firstSetResult).to.have.lengthOf(4);
|
||||
|
||||
user.stats.buffs.seafoam = false;
|
||||
user.stats.buffs.spookySparkles = false;
|
||||
user.stats.buffs.snowball = false;
|
||||
user.stats.buffs.shinySeed = false;
|
||||
|
||||
const secondSetResult = [...setDebuffPotionItems(user).pinnedItems];
|
||||
|
||||
expect(secondSetResult).to.have.lengthOf(0);
|
||||
});
|
||||
});
|
||||
@@ -41,7 +41,10 @@ describe('shared.ops.buyMarketGear', () => {
|
||||
},
|
||||
});
|
||||
|
||||
user.addAchievement = sinon.spy();
|
||||
|
||||
sinon.stub(shared, 'randomVal');
|
||||
sinon.stub(shared.onboarding, 'checkOnboardingStatus');
|
||||
sinon.stub(shared.fns, 'predictableRandom');
|
||||
sinon.stub(analytics, 'track');
|
||||
});
|
||||
@@ -49,6 +52,7 @@ describe('shared.ops.buyMarketGear', () => {
|
||||
afterEach(() => {
|
||||
shared.randomVal.restore();
|
||||
shared.fns.predictableRandom.restore();
|
||||
shared.onboarding.checkOnboardingStatus.restore();
|
||||
analytics.track.restore();
|
||||
});
|
||||
|
||||
@@ -86,6 +90,27 @@ describe('shared.ops.buyMarketGear', () => {
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('adds the onboarding achievement to the user and checks the onboarding status', () => {
|
||||
user.stats.gp = 31;
|
||||
|
||||
buyGear(user, { params: { key: 'armor_warrior_1' } }, analytics);
|
||||
|
||||
expect(user.addAchievement).to.be.calledOnce;
|
||||
expect(user.addAchievement).to.be.calledWith('purchasedEquipment');
|
||||
|
||||
expect(shared.onboarding.checkOnboardingStatus).to.be.calledOnce;
|
||||
expect(shared.onboarding.checkOnboardingStatus).to.be.calledWith(user);
|
||||
});
|
||||
|
||||
it('does not add the onboarding achievement to the user if it\'s already been awarded', () => {
|
||||
user.stats.gp = 31;
|
||||
user.achievements.purchasedEquipment = true;
|
||||
|
||||
buyGear(user, { params: { key: 'armor_warrior_1' } }, analytics);
|
||||
|
||||
expect(user.addAchievement).to.not.be.called;
|
||||
});
|
||||
|
||||
it('deducts gold from user', () => {
|
||||
user.stats.gp = 31;
|
||||
|
||||
|
||||
@@ -10,12 +10,19 @@ import {
|
||||
generateUser,
|
||||
} from '../../helpers/common.helper';
|
||||
import errorMessage from '../../../website/common/script/libs/errorMessage';
|
||||
import shared from '../../../website/common/script';
|
||||
|
||||
describe('shared.ops.feed', () => {
|
||||
let user;
|
||||
|
||||
beforeEach(() => {
|
||||
user = generateUser();
|
||||
user.addAchievement = sinon.spy();
|
||||
sinon.stub(shared.onboarding, 'checkOnboardingStatus');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
shared.onboarding.checkOnboardingStatus.restore();
|
||||
});
|
||||
|
||||
context('failure conditions', () => {
|
||||
@@ -223,5 +230,28 @@ describe('shared.ops.feed', () => {
|
||||
expect(user.items.mounts['Wolf-Base']).to.equal(true);
|
||||
expect(user.items.currentPet).to.equal('');
|
||||
});
|
||||
|
||||
it('adds the onboarding achievement to the user and checks the onboarding status', () => {
|
||||
user.items.pets['Wolf-Base'] = 5;
|
||||
user.items.food.Meat = 2;
|
||||
|
||||
feed(user, { params: { pet: 'Wolf-Base', food: 'Meat' } });
|
||||
|
||||
expect(user.addAchievement).to.be.calledOnce;
|
||||
expect(user.addAchievement).to.be.calledWith('fedPet');
|
||||
|
||||
expect(shared.onboarding.checkOnboardingStatus).to.be.calledOnce;
|
||||
expect(shared.onboarding.checkOnboardingStatus).to.be.calledWith(user);
|
||||
});
|
||||
|
||||
it('does not add the onboarding achievement to the user if it\'s already been awarded', () => {
|
||||
user.items.pets['Wolf-Base'] = 5;
|
||||
user.items.food.Meat = 2;
|
||||
user.achievements.fedPet = true;
|
||||
|
||||
feed(user, { params: { pet: 'Wolf-Base', food: 'Meat' } });
|
||||
|
||||
expect(user.addAchievement).to.not.be.called;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,12 +9,19 @@ import {
|
||||
generateUser,
|
||||
} from '../../helpers/common.helper';
|
||||
import errorMessage from '../../../website/common/script/libs/errorMessage';
|
||||
import shared from '../../../website/common/script';
|
||||
|
||||
describe('shared.ops.hatch', () => {
|
||||
let user;
|
||||
|
||||
beforeEach(() => {
|
||||
user = generateUser();
|
||||
user.addAchievement = sinon.spy();
|
||||
sinon.stub(shared.onboarding, 'checkOnboardingStatus');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
shared.onboarding.checkOnboardingStatus.restore();
|
||||
});
|
||||
|
||||
context('Pet Hatching', () => {
|
||||
@@ -195,6 +202,30 @@ describe('shared.ops.hatch', () => {
|
||||
hatch(user, { params: { egg: 'Wolf', hatchingPotion: 'Spooky' } });
|
||||
expect(user.achievements.dustDevil).to.eql(true);
|
||||
});
|
||||
|
||||
it('adds the onboarding achievement to the user and checks the onboarding status', () => {
|
||||
user.items.eggs = { Wolf: 1 };
|
||||
user.items.hatchingPotions = { Base: 1 };
|
||||
user.items.pets = {};
|
||||
hatch(user, { params: { egg: 'Wolf', hatchingPotion: 'Base' } });
|
||||
|
||||
expect(user.addAchievement).to.be.calledOnce;
|
||||
expect(user.addAchievement).to.be.calledWith('hatchedPet');
|
||||
|
||||
expect(shared.onboarding.checkOnboardingStatus).to.be.calledOnce;
|
||||
expect(shared.onboarding.checkOnboardingStatus).to.be.calledWith(user);
|
||||
});
|
||||
|
||||
it('does not add the onboarding achievement to the user if it\'s already been awarded', () => {
|
||||
user.items.eggs = { Wolf: 1 };
|
||||
user.items.hatchingPotions = { Base: 1 };
|
||||
user.items.pets = {};
|
||||
user.achievements.hatchedPet = true;
|
||||
|
||||
hatch(user, { params: { egg: 'Wolf', hatchingPotion: 'Base' } });
|
||||
|
||||
expect(user.addAchievement).to.not.be.called;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
NotAuthorized,
|
||||
} from '../../../website/common/script/libs/errors';
|
||||
import crit from '../../../website/common/script/fns/crit';
|
||||
import shared from '../../../website/common/script';
|
||||
|
||||
const EPSILON = 0.0001; // negligible distance between datapoints
|
||||
|
||||
@@ -340,5 +341,49 @@ describe('shared.ops.scoreTask', () => {
|
||||
expectClosePoints(ref.beforeUser, ref.afterUser, freshTodo, todo);
|
||||
});
|
||||
});
|
||||
|
||||
context('onboarding', () => {
|
||||
beforeEach(() => {
|
||||
ref.afterUser.addAchievement = sinon.spy();
|
||||
sinon.stub(shared.onboarding, 'checkOnboardingStatus');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
shared.onboarding.checkOnboardingStatus.restore();
|
||||
});
|
||||
|
||||
it('adds the achievement to the user and checks the onboarding status', () => {
|
||||
scoreTask({ user: ref.afterUser, task: todo, direction: 'up' });
|
||||
expect(ref.afterUser.addAchievement).to.be.calledOnce;
|
||||
expect(ref.afterUser.addAchievement).to.be.calledWith('completedTask');
|
||||
|
||||
expect(shared.onboarding.checkOnboardingStatus).to.be.calledOnce;
|
||||
expect(shared.onboarding.checkOnboardingStatus).to.be.calledWith(ref.afterUser);
|
||||
});
|
||||
|
||||
it('does not add the onboarding achievement to the user if it\'s already been awarded', () => {
|
||||
ref.afterUser.achievements.completedTask = true;
|
||||
scoreTask({ user: ref.afterUser, task: todo, direction: 'up' });
|
||||
|
||||
expect(ref.afterUser.addAchievement).to.not.be.called;
|
||||
});
|
||||
|
||||
it('does not add the onboarding achievement to the user if it\'s scored down', () => {
|
||||
scoreTask({ user: ref.afterUser, task: todo, direction: 'down' });
|
||||
|
||||
expect(ref.afterUser.addAchievement).to.not.be.called;
|
||||
});
|
||||
|
||||
it('does not add the onboarding achievement to the user if cron is running', () => {
|
||||
scoreTask({
|
||||
user: ref.afterUser,
|
||||
task: todo,
|
||||
direction: 'up',
|
||||
cron: true,
|
||||
});
|
||||
|
||||
expect(ref.afterUser.addAchievement).to.not.be.called;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ module.exports = {
|
||||
extends: [
|
||||
'habitrpg/lib/vue',
|
||||
],
|
||||
ignorePatterns: ['dist/', 'node_modules/'],
|
||||
rules: {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# new_client
|
||||
# Habitica Client
|
||||
|
||||
## Project setup
|
||||
```
|
||||
@@ -27,3 +27,31 @@ npm run lint
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||
|
||||
## Storybook
|
||||
|
||||
Storybook is mainly used while working on UI-Components to see changes faster instead of using the website.
|
||||
|
||||
### Start Storybook
|
||||
|
||||
```
|
||||
npm run storybook:serve
|
||||
```
|
||||
|
||||
This will start the storybook process, every `*.stories.js`-File is searched and added to the storybook overview.
|
||||
|
||||
### Storybook Worklow
|
||||
|
||||
Usually when you working on `component-name.vue` you also create a `component-name.stories.js` file.
|
||||
|
||||
Example of the stories structure - [Storybook Docs][StorybookDocsExample] - [CountBadge][CountBadgeExample]
|
||||
|
||||
[StorybookDocsExample]: https://storybook.js.org/docs/guides/guide-vue/#step-4-write-your-stories
|
||||
[CountBadgeExample]: src/components/ui/countBadge.stories.js
|
||||
|
||||
Each function or example of this component will be put after `storiesOf('Your Component', module)`,
|
||||
in a separate `.add('function of component', ...`
|
||||
|
||||
### Storybook Build
|
||||
|
||||
After each client build, storybook build is also triggered and will be available in `dist/storybook`
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import '@storybook/addon-actions/register';
|
||||
import '@storybook/addon-knobs/register';
|
||||
import '@storybook/addon-links/register';
|
||||
import '@storybook/addon-notes/register';
|
||||
@@ -0,0 +1,11 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import { configure } from '@storybook/vue';
|
||||
import '../../src/assets/scss/index.scss';
|
||||
|
||||
const req = require.context('../../src', true, /.stories.js$/);
|
||||
|
||||
function loadStories () {
|
||||
req.keys().forEach(filename => req(filename));
|
||||
}
|
||||
|
||||
configure(loadStories, module);
|
||||
@@ -5,30 +5,38 @@
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"test:unit": "vue-cli-service test:unit --require ./tests/unit/helpers.js",
|
||||
"lint": "vue-cli-service lint .",
|
||||
"postinstall": "node ./scripts/npm-postinstall.js"
|
||||
"lint-no-fix": "vue-cli-service lint --no-fix .",
|
||||
"postinstall": "node ./scripts/npm-postinstall.js",
|
||||
"storybook:build": "vue-cli-service storybook:build -c config/storybook -o dist/storybook",
|
||||
"storybook:serve": "vue-cli-service storybook:serve -p 6006 -c config/storybook",
|
||||
"test:unit": "vue-cli-service test:unit --require ./tests/unit/helpers.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vue/cli-plugin-babel": "^4.0.5",
|
||||
"@vue/cli-plugin-eslint": "^4.0.5",
|
||||
"@vue/cli-plugin-router": "^4.0.5",
|
||||
"@vue/cli-plugin-unit-mocha": "^4.0.5",
|
||||
"@vue/cli-service": "^4.0.5",
|
||||
"@vue/cli-plugin-babel": "^4.1.1",
|
||||
"@vue/cli-plugin-eslint": "^4.1.1",
|
||||
"@vue/cli-plugin-router": "^4.1.1",
|
||||
"@vue/cli-plugin-unit-mocha": "^4.1.1",
|
||||
"@vue/cli-service": "^4.1.1",
|
||||
"@storybook/addon-actions": "^5.0.0",
|
||||
"@storybook/addon-knobs": "^5.0.0",
|
||||
"@storybook/addon-links": "^5.0.0",
|
||||
"@storybook/addon-notes": "^5.0.0",
|
||||
"@storybook/vue": "^5.2.5",
|
||||
"@vue/test-utils": "1.0.0-beta.29",
|
||||
"amplitude-js": "^5.6.0",
|
||||
"amplitude-js": "^5.8.0",
|
||||
"axios": "^0.19.0",
|
||||
"axios-progress-bar": "^1.2.0",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"bootstrap": "^4.3.1",
|
||||
"bootstrap-vue": "^2.0.4",
|
||||
"bootstrap": "^4.4.1",
|
||||
"bootstrap-vue": "^2.1.0",
|
||||
"chai": "^4.1.2",
|
||||
"core-js": "^3.3.4",
|
||||
"eslint": "^6.6.0",
|
||||
"core-js": "^3.5.0",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-config-habitrpg": "^6.2.0",
|
||||
"eslint-plugin-mocha": "^5.3.0",
|
||||
"eslint-plugin-vue": "^5.0.0",
|
||||
"habitica-markdown": "^1.3.0",
|
||||
"eslint-plugin-vue": "^6.0.1",
|
||||
"habitica-markdown": "^1.3.2",
|
||||
"hellojs": "^1.18.1",
|
||||
"inspectpack": "^4.2.2",
|
||||
"intro.js": "^2.9.3",
|
||||
@@ -36,19 +44,20 @@
|
||||
"lodash": "^4.17.15",
|
||||
"moment": "^2.24.0",
|
||||
"nconf": "^0.10.0",
|
||||
"sass": "^1.23.1",
|
||||
"sass": "^1.23.7",
|
||||
"sass-loader": "^8.0.0",
|
||||
"smartbanner.js": "^1.14.5",
|
||||
"smartbanner.js": "^1.15.0",
|
||||
"svg-inline-loader": "^0.8.0",
|
||||
"svg-url-loader": "^3.0.2",
|
||||
"svgo": "^1.3.0",
|
||||
"svg-url-loader": "^3.0.3",
|
||||
"svgo": "^1.3.2",
|
||||
"svgo-loader": "^2.2.1",
|
||||
"uuid": "^3.3.3",
|
||||
"validator": "^11.1.0",
|
||||
"vue": "^2.6.10",
|
||||
"vue": "^2.6.11",
|
||||
"vue-cli-plugin-storybook": "^0.6.1",
|
||||
"vue-mugen-scroll": "^0.2.6",
|
||||
"vue-router": "^3.0.6",
|
||||
"vue-template-compiler": "^2.6.10",
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
"vuedraggable": "^2.23.1",
|
||||
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#5d237615463a84a23dd6f3f77c6ab577d68593ec",
|
||||
"webpack": "^4.41.2"
|
||||
|
||||
@@ -6,4 +6,8 @@ if (process.env.NODE_ENV === 'production') {
|
||||
execSync('npm run build', {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
execSync('npm run storybook:build', {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -64,6 +64,30 @@
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="g1g1-banner d-flex justify-content-center align-items-center"
|
||||
v-if="!giftingHidden">
|
||||
<div
|
||||
class="svg-icon svg-gifts left-gift"
|
||||
v-html="icons.gifts">
|
||||
</div>
|
||||
<router-link class="g1g1-link" to="/user/settings/subscription">
|
||||
{{ $t('g1g1Announcement') }}
|
||||
</router-link>
|
||||
<div
|
||||
class="svg-icon svg-gifts right-gift"
|
||||
v-html="icons.gifts">
|
||||
</div>
|
||||
<div
|
||||
class="closepadding"
|
||||
@click="hideGiftingBanner()">
|
||||
<span
|
||||
class="svg-icon inline-icon icon-10"
|
||||
aria-hidden="true"
|
||||
v-html="icons.close">
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<notifications-display />
|
||||
<app-menu />
|
||||
<div class="container-fluid">
|
||||
@@ -135,10 +159,43 @@
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.closepadding {
|
||||
margin: 11px 24px;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
right: 0;
|
||||
top: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.container-fluid {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.g1g1-banner {
|
||||
width: 100%;
|
||||
min-height: 2.5rem;
|
||||
background-color: $teal-50;
|
||||
}
|
||||
|
||||
.g1g1-link {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.left-gift {
|
||||
margin: auto 1rem auto auto;
|
||||
}
|
||||
|
||||
.right-gift {
|
||||
margin: auto auto auto 1rem;
|
||||
filter: flipH;
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
.svg-gifts {
|
||||
width: 4.6rem;
|
||||
}
|
||||
|
||||
.notification {
|
||||
border-radius: 1000px;
|
||||
background-color: $green-10;
|
||||
@@ -165,15 +222,6 @@
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.closepadding {
|
||||
margin: 11px 24px;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
right: 0;
|
||||
top: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.content {
|
||||
font-size: 12px;
|
||||
@@ -239,8 +287,14 @@ import subCancelModalConfirm from '@/components/payments/cancelModalConfirm';
|
||||
import subCanceledModal from '@/components/payments/canceledModal';
|
||||
|
||||
import spellsMixin from '@/mixins/spells';
|
||||
import { CONSTANTS, getLocalSetting, removeLocalSetting } from '@/libs/userlocalManager';
|
||||
import {
|
||||
CONSTANTS,
|
||||
getLocalSetting,
|
||||
removeLocalSetting,
|
||||
setLocalSetting,
|
||||
} from '@/libs/userlocalManager';
|
||||
|
||||
import gifts from '@/assets/svg/gifts.svg';
|
||||
import svgClose from '@/assets/svg/close.svg';
|
||||
import bannedAccountModal from '@/components/bannedAccountModal';
|
||||
|
||||
@@ -267,6 +321,7 @@ export default {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
close: svgClose,
|
||||
gifts,
|
||||
}),
|
||||
selectedItemToBuy: null,
|
||||
selectedSpellToBuy: null,
|
||||
@@ -277,6 +332,7 @@ export default {
|
||||
loading: true,
|
||||
currentTipNumber: 0,
|
||||
bannerHidden: false,
|
||||
giftingHidden: getLocalSetting(CONSTANTS.keyConstants.GIFTING_BANNER_DISPLAY) === 'dismissed2019',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -670,6 +726,10 @@ export default {
|
||||
hideBanner () {
|
||||
this.bannerHidden = true;
|
||||
},
|
||||
hideGiftingBanner () {
|
||||
setLocalSetting(CONSTANTS.keyConstants.GIFTING_BANNER_DISPLAY, 'dismissed2019');
|
||||
this.giftingHidden = true;
|
||||
},
|
||||
resumeDamage () {
|
||||
this.$store.dispatch('user:sleep');
|
||||
},
|
||||
|
||||
@@ -1,30 +1,60 @@
|
||||
.promo_armoire_backgrounds_201911 {
|
||||
.promo_achievement_white {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -477px 0px;
|
||||
background-position: -524px -444px;
|
||||
width: 204px;
|
||||
height: 102px;
|
||||
}
|
||||
.promo_armoire_backgrounds_201912 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -148px;
|
||||
width: 423px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_costume_achievement {
|
||||
.promo_g1g1_2019 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -365px;
|
||||
width: 144px;
|
||||
height: 156px;
|
||||
background-position: -469px -296px;
|
||||
width: 357px;
|
||||
height: 144px;
|
||||
}
|
||||
.promo_mystery_201910 {
|
||||
.promo_mystery_201912 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -477px -148px;
|
||||
background-position: -241px -444px;
|
||||
width: 282px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_take_this {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -760px -148px;
|
||||
background-position: -729px -444px;
|
||||
width: 96px;
|
||||
height: 69px;
|
||||
}
|
||||
.scene_seaserpent {
|
||||
.promo_winter_potions_2020 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -296px;
|
||||
width: 423px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_winter_quests_bundle {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -469px 0px;
|
||||
width: 423px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_winter_wonderland_2019 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -469px -148px;
|
||||
width: 402px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_winter_wonderland_2020 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px 0px;
|
||||
width: 476px;
|
||||
height: 364px;
|
||||
width: 468px;
|
||||
height: 147px;
|
||||
}
|
||||
.scene_todos {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -444px;
|
||||
width: 240px;
|
||||
height: 195px;
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 481 KiB After Width: | Height: | Size: 477 KiB |
|
Before Width: | Height: | Size: 669 KiB After Width: | Height: | Size: 688 KiB |
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 369 KiB After Width: | Height: | Size: 346 KiB |
|
Before Width: | Height: | Size: 317 KiB After Width: | Height: | Size: 335 KiB |
|
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 159 KiB |
|
Before Width: | Height: | Size: 141 KiB After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 175 KiB After Width: | Height: | Size: 173 KiB |
|
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 149 KiB After Width: | Height: | Size: 149 KiB |
|
Before Width: | Height: | Size: 154 KiB After Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 145 KiB After Width: | Height: | Size: 143 KiB |
|
Before Width: | Height: | Size: 182 KiB After Width: | Height: | Size: 163 KiB |
|
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 178 KiB |
|
Before Width: | Height: | Size: 164 KiB After Width: | Height: | Size: 159 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 126 KiB |