diff --git a/Dockerfile-Dev b/Dockerfile-Dev index a9316937eb..efb8bd074e 100644 --- a/Dockerfile-Dev +++ b/Dockerfile-Dev @@ -3,10 +3,13 @@ FROM node:20 # Install global packages RUN npm install -g gulp-cli mocha -# Copy package.json and package-lock.json into image, then install -# dependencies. +# Copy package.json and package-lock.json into image WORKDIR /usr/src/habitica COPY ["package.json", "package-lock.json", "./"] # Copy the remaining source files in. COPY . /usr/src/habitica +# Install dependencies RUN npm install +RUN npm run postinstall +RUN npm run client:build +RUN gulp build:prod diff --git a/package.json b/package.json index f166b266f1..1afd5c6c61 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,9 @@ "debug": "gulp nodemon --inspect", "mongo:dev": "run-rs -v 5.0.23 -l ubuntu1804 --keep --dbpath mongodb-data --number 1 --quiet", "postinstall": "git config --global url.\"https://\".insteadOf git:// && gulp build && cd website/client && npm install", - "apidoc": "gulp apidoc" + "apidoc": "gulp apidoc", + + "heroku-postbuild": "npm run client:build" }, "devDependencies": { "axios": "^1.4.0", diff --git a/test/api/unit/libs/content.test.js b/test/api/unit/libs/content.test.js index 53c85e4205..2db4e13859 100644 --- a/test/api/unit/libs/content.test.js +++ b/test/api/unit/libs/content.test.js @@ -55,7 +55,7 @@ describe('contentLib', () => { beforeEach(() => { resSpy = generateRes(); if (fs.existsSync(contentLib.CONTENT_CACHE_PATH)) { - fs.rmdirSync(contentLib.CONTENT_CACHE_PATH, { recursive: true }); + fs.rmSync(contentLib.CONTENT_CACHE_PATH, { recursive: true }); } fs.mkdirSync(contentLib.CONTENT_CACHE_PATH); }); diff --git a/test/common/ops/armoireCanOwn.js b/test/common/ops/armoireCanOwn.js index a226c91df2..74b48367ec 100644 --- a/test/common/ops/armoireCanOwn.js +++ b/test/common/ops/armoireCanOwn.js @@ -3,6 +3,7 @@ import armoireSet from '../../../website/common/script/content/gear/sets/armoire describe('armoireSet items', () => { it('checks if canOwn has the same id', () => { Object.keys(armoireSet).forEach(type => { + if (type === 'all') return; Object.keys(armoireSet[type]).forEach(itemKey => { const ownedKey = `${type}_armoire_${itemKey}`; expect(armoireSet[type][itemKey].canOwn({ diff --git a/test/content/armoire.test.js b/test/content/armoire.test.js index cbcb0e253c..02e718eca6 100644 --- a/test/content/armoire.test.js +++ b/test/content/armoire.test.js @@ -3,38 +3,26 @@ import forEach from 'lodash/forEach'; import { expectValidTranslationString, } from '../helpers/content.helper'; - -function makeArmoireIitemList () { - const armoire = require('../../website/common/script/content/gear/sets/armoire').default; - const items = []; - items.push(...Object.values(armoire.armor)); - items.push(...Object.values(armoire.body)); - items.push(...Object.values(armoire.eyewear)); - items.push(...Object.values(armoire.head)); - items.push(...Object.values(armoire.headAccessory)); - items.push(...Object.values(armoire.shield)); - items.push(...Object.values(armoire.weapon)); - return items; -} +import armoire from '../../website/common/script/content/gear/sets/armoire'; describe('armoire', () => { let clock; - beforeEach(() => { - delete require.cache[require.resolve('../../website/common/script/content/gear/sets/armoire')]; - }); afterEach(() => { - clock.restore(); + if (clock) { + clock.restore(); + } }); + it('does not return unreleased gear', async () => { clock = sinon.useFakeTimers(new Date('2024-01-02')); - const items = makeArmoireIitemList(); + const items = armoire.all; expect(items.length).to.equal(377); expect(items.filter(item => item.set === 'pottersSet' || item.set === 'optimistSet' || item.set === 'schoolUniform')).to.be.an('array').that.is.empty; }); it('released gear has all required properties', async () => { clock = sinon.useFakeTimers(new Date('2024-05-08')); - const items = makeArmoireIitemList(); + const items = armoire.all; expect(items.length).to.equal(396); forEach(items, item => { if (item.set !== undefined) { @@ -48,29 +36,30 @@ describe('armoire', () => { it('releases gear when appropriate', async () => { clock = sinon.useFakeTimers(new Date('2024-01-01T00:00:00.000Z')); - const items = makeArmoireIitemList(); + const items = armoire.all; expect(items.length).to.equal(377); clock.restore(); delete require.cache[require.resolve('../../website/common/script/content/gear/sets/armoire')]; clock = sinon.useFakeTimers(new Date('2024-01-08')); - const januaryItems = makeArmoireIitemList(); + const januaryItems = armoire.all; expect(januaryItems.length).to.equal(381); clock.restore(); delete require.cache[require.resolve('../../website/common/script/content/gear/sets/armoire')]; clock = sinon.useFakeTimers(new Date('2024-02-07')); - const januaryItems2 = makeArmoireIitemList(); + const januaryItems2 = armoire.all; expect(januaryItems2.length).to.equal(381); clock.restore(); delete require.cache[require.resolve('../../website/common/script/content/gear/sets/armoire')]; - clock = sinon.useFakeTimers(new Date('2024-02-07T16:00:00.000Z')); - const febuaryItems = makeArmoireIitemList(); + clock = sinon.useFakeTimers(new Date('2024-02-07T09:00:00.000Z')); + const febuaryItems = armoire.all; expect(febuaryItems.length).to.equal(384); }); it('sets have at least 2 items', () => { - const armoire = makeArmoireIitemList(); const setMap = {}; - forEach(armoire, item => { + forEach(armoire.all, item => { + // Gotta have one outlier + if (!item.set || item.set.startsWith('armoire-')) return; if (setMap[item.set] === undefined) { setMap[item.set] = 0; } diff --git a/test/content/eggs.test.js b/test/content/eggs.test.js index cb0e173579..69dcb47798 100644 --- a/test/content/eggs.test.js +++ b/test/content/eggs.test.js @@ -5,29 +5,51 @@ import { expectValidTranslationString, } from '../helpers/content.helper'; -import * as eggs from '../../website/common/script/content/eggs'; +import eggs from '../../website/common/script/content/eggs'; describe('eggs', () => { - describe('all', () => { - it('is a combination of drop and quest eggs', () => { - const dropNumber = Object.keys(eggs.drops).length; - const questNumber = Object.keys(eggs.quests).length; - const allNumber = Object.keys(eggs.all).length; + let clock; - expect(allNumber).to.be.greaterThan(0); - expect(allNumber).to.equal(dropNumber + questNumber); - }); + afterEach(() => { + if (clock) { + clock.restore(); + } + }); - it('contains basic information about each egg', () => { - each(eggs.all, (egg, key) => { - expectValidTranslationString(egg.text); - expectValidTranslationString(egg.adjective); - expectValidTranslationString(egg.mountText); - expectValidTranslationString(egg.notes); - expect(egg.canBuy).to.be.a('function'); - expect(egg.value).to.be.a('number'); - expect(egg.key).to.equal(key); + const eggTypes = [ + 'drops', + 'quests', + ]; + + eggTypes.forEach(eggType => { + describe(eggType, () => { + it('contains basic information about each egg', () => { + each(eggs[eggType], (egg, key) => { + expectValidTranslationString(egg.text); + expectValidTranslationString(egg.adjective); + expectValidTranslationString(egg.mountText); + expectValidTranslationString(egg.notes); + expect(egg.canBuy).to.be.a('function'); + expect(egg.value).to.be.a('number'); + expect(egg.key).to.equal(key); + }); }); }); }); + + it('does not contain unreleased eggs', () => { + clock = sinon.useFakeTimers(new Date('2024-05-20')); + const questEggs = eggs.quests; + expect(questEggs.Giraffe).to.not.exist; + }); + + it('Releases eggs when appropriate without needing restarting', () => { + clock = sinon.useFakeTimers(new Date('2024-05-20')); + const mayEggs = eggs.quests; + clock.restore(); + clock = sinon.useFakeTimers(new Date('2024-06-20')); + const juneEggs = eggs.quests; + expect(juneEggs.Giraffe).to.exist; + expect(Object.keys(mayEggs).length).to.equal(Object.keys(juneEggs).length - 1); + }); }); diff --git a/test/content/index.test.js b/test/content/index.test.js new file mode 100644 index 0000000000..54ea054b64 --- /dev/null +++ b/test/content/index.test.js @@ -0,0 +1,154 @@ +import content from '../../website/common/script/content'; + +describe('content index', () => { + let clock; + + afterEach(() => { + if (clock) { + clock.restore(); + } + }); + + it('Releases eggs when appropriate without needing restarting', () => { + clock = sinon.useFakeTimers(new Date('2024-06-20')); + const mayEggs = content.eggs; + expect(mayEggs.Chameleon).to.not.exist; + clock.restore(); + clock = sinon.useFakeTimers(new Date('2024-07-20')); + const juneEggs = content.eggs; + expect(juneEggs.Chameleon).to.exist; + expect(Object.keys(mayEggs).length, '').to.equal(Object.keys(juneEggs).length - 1); + }); + + it('Releases hatching potions when appropriate without needing restarting', () => { + clock = sinon.useFakeTimers(new Date('2024-05-20')); + const mayHatchingPotions = content.hatchingPotions; + expect(mayHatchingPotions.Koi).to.not.exist; + clock.restore(); + clock = sinon.useFakeTimers(new Date('2024-06-20')); + const juneHatchingPotions = content.hatchingPotions; + expect(juneHatchingPotions.Koi).to.exist; + expect(Object.keys(mayHatchingPotions).length, '').to.equal(Object.keys(juneHatchingPotions).length - 1); + }); + + it('Releases armoire gear when appropriate without needing restarting', () => { + clock = sinon.useFakeTimers(new Date('2024-06-20')); + const juneGear = content.gear.flat; + expect(juneGear.armor_armoire_corsairsCoatAndCape).to.not.exist; + clock.restore(); + clock = sinon.useFakeTimers(new Date('2024-07-10')); + const julyGear = content.gear.flat; + expect(julyGear.armor_armoire_corsairsCoatAndCape).to.exist; + expect(Object.keys(juneGear).length, '').to.equal(Object.keys(julyGear).length - 3); + }); + + it('Releases pets gear when appropriate without needing restarting', () => { + clock = sinon.useFakeTimers(new Date('2024-06-20')); + const junePets = content.petInfo; + expect(junePets['Chameleon-Base']).to.not.exist; + clock.restore(); + clock = sinon.useFakeTimers(new Date('2024-07-10')); + const julyPets = content.petInfo; + expect(julyPets['Chameleon-Base']).to.exist; + expect(Object.keys(junePets).length, '').to.equal(Object.keys(julyPets).length - 10); + }); + + it('Releases mounts gear when appropriate without needing restarting', () => { + clock = sinon.useFakeTimers(new Date('2024-06-20')); + const juneMounts = content.mountInfo; + expect(juneMounts['Chameleon-Base']).to.not.exist; + clock.restore(); + clock = sinon.useFakeTimers(new Date('2024-07-10')); + const julyMounts = content.mountInfo; + expect(julyMounts['Chameleon-Base']).to.exist; + expect(Object.keys(juneMounts).length, '').to.equal(Object.keys(julyMounts).length - 10); + }); + + it('marks regular food as buyable and droppable without any events', () => { + clock = sinon.useFakeTimers(new Date('2024-06-20')); + const { food } = content; + Object.keys(food).forEach(key => { + if (key === 'Saddle') { + expect(food[key].canBuy(), `${key} canBuy`).to.be.true; + expect(food[key].canDrop, `${key} canDrop`).to.be.false; + return; + } + let expected = true; + if (key.startsWith('Cake_')) { + expected = false; + } else if (key.startsWith('Candy_')) { + expected = false; + } else if (key.startsWith('Pie_')) { + expected = false; + } + expect(food[key].canBuy(), `${key} canBuy`).to.equal(expected); + expect(food[key].canDrop, `${key} canDrop`).to.equal(expected); + }); + }); + + it('marks candy as buyable and droppable during habitoween', () => { + clock = sinon.useFakeTimers(new Date('2024-10-31')); + const { food } = content; + Object.keys(food).forEach(key => { + if (key === 'Saddle') { + expect(food[key].canBuy(), `${key} canBuy`).to.be.true; + expect(food[key].canDrop, `${key} canDrop`).to.be.false; + return; + } + let expected = false; + if (key.startsWith('Cake_')) { + expected = false; + } else if (key.startsWith('Candy_')) { + expected = true; + } else if (key.startsWith('Pie_')) { + expected = false; + } + expect(food[key].canBuy(), `${key} canBuy`).to.equal(expected); + expect(food[key].canDrop, `${key} canDrop`).to.equal(expected); + }); + }); + + it('marks cake as buyable and droppable during birthday', () => { + clock = sinon.useFakeTimers(new Date('2024-01-31')); + const { food } = content; + Object.keys(food).forEach(key => { + if (key === 'Saddle') { + expect(food[key].canBuy(), `${key} canBuy`).to.be.true; + expect(food[key].canDrop, `${key} canDrop`).to.be.false; + return; + } + let expected = false; + if (key.startsWith('Cake_')) { + expected = true; + } else if (key.startsWith('Candy_')) { + expected = false; + } else if (key.startsWith('Pie_')) { + expected = false; + } + expect(food[key].canBuy(), `${key} canBuy`).to.equal(expected); + expect(food[key].canDrop, `${key} canDrop`).to.equal(expected); + }); + }); + + it('marks pie as buyable and droppable during pi day', () => { + clock = sinon.useFakeTimers(new Date('2024-03-14')); + const { food } = content; + Object.keys(food).forEach(key => { + if (key === 'Saddle') { + expect(food[key].canBuy(), `${key} canBuy`).to.be.true; + expect(food[key].canDrop, `${key} canDrop`).to.be.false; + return; + } + let expected = false; + if (key.startsWith('Cake_')) { + expected = false; + } else if (key.startsWith('Candy_')) { + expected = false; + } else if (key.startsWith('Pie_')) { + expected = true; + } + expect(food[key].canBuy(), `${key} canBuy`).to.equal(expected); + expect(food[key].canDrop, `${key} canDrop`).to.equal(expected); + }); + }); +}); diff --git a/test/content/releaseDates.test.js b/test/content/releaseDates.test.js new file mode 100644 index 0000000000..1c5fad92b3 --- /dev/null +++ b/test/content/releaseDates.test.js @@ -0,0 +1,82 @@ +import find from 'lodash/find'; +import maxBy from 'lodash/maxBy'; +import { + ARMOIRE_RELEASE_DATES, + EGGS_RELEASE_DATES, + HATCHING_POTIONS_RELEASE_DATES, +} from '../../website/common/script/content/constants/releaseDates'; +import armoire from '../../website/common/script/content/gear/sets/armoire'; +import eggs from '../../website/common/script/content/eggs'; +import hatchingPotions from '../../website/common/script/content/hatching-potions'; + +describe('releaseDates', () => { + let clock; + + afterEach(() => { + if (clock) { + clock.restore(); + } + }); + describe('armoire', () => { + it('should only contain valid armoire names', () => { + const lastReleaseDate = maxBy(Object.values(ARMOIRE_RELEASE_DATES), value => new Date(`${value.year}-${value.month + 1}-20`)); + clock = sinon.useFakeTimers(new Date(`${lastReleaseDate.year}-${lastReleaseDate.month + 1}-20`)); + Object.keys(ARMOIRE_RELEASE_DATES).forEach(key => { + expect(find(armoire.all, { set: key }), `${key} is not a valid armoire set`).to.exist; + }); + }); + + it('should contain a valid year and month', () => { + Object.keys(ARMOIRE_RELEASE_DATES).forEach(key => { + const date = ARMOIRE_RELEASE_DATES[key]; + expect(date.year, `${key} year is not a valid year`).to.be.a('number'); + expect(date.year).to.be.at.least(2023); + expect(date.month, `${key} month is not a valid month`).to.be.a('number'); + expect(date.month).to.be.within(1, 12); + expect(date.day).to.not.exist; + }); + }); + }); + + describe('eggs', () => { + it('should only contain valid egg names', () => { + const lastReleaseDate = maxBy(Object.values(EGGS_RELEASE_DATES), value => new Date(`${value.year}-${value.month + 1}-${value.day}`)); + clock = sinon.useFakeTimers(new Date(`${lastReleaseDate.year}-${lastReleaseDate.month + 1}-${lastReleaseDate.day}`)); + Object.keys(EGGS_RELEASE_DATES).forEach(key => { + expect(eggs.all[key], `${key} is not a valid egg name`).to.exist; + }); + }); + + it('should contain a valid year, month and date', () => { + Object.keys(EGGS_RELEASE_DATES).forEach(key => { + const date = EGGS_RELEASE_DATES[key]; + expect(date.year, `${key} year is not a valid year`).to.be.a('number'); + expect(date.year).to.be.at.least(2024); + expect(date.month, `${key} month is not a valid month`).to.be.a('number'); + expect(date.month).to.be.within(1, 12); + expect(date.day, `${key} day is not a valid day`).to.be.a('number'); + }); + }); + }); + + describe('hatchingPotions', () => { + it('should only contain valid potion names', () => { + const lastReleaseDate = maxBy(Object.values(HATCHING_POTIONS_RELEASE_DATES), value => new Date(`${value.year}-${value.month + 1}-${value.day}`)); + clock = sinon.useFakeTimers(new Date(`${lastReleaseDate.year}-${lastReleaseDate.month + 1}-${lastReleaseDate.day}`)); + Object.keys(HATCHING_POTIONS_RELEASE_DATES).forEach(key => { + expect(hatchingPotions.all[key], `${key} is not a valid potion name`).to.exist; + }); + }); + + it('should contain a valid year, month and date', () => { + Object.keys(HATCHING_POTIONS_RELEASE_DATES).forEach(key => { + const date = HATCHING_POTIONS_RELEASE_DATES[key]; + expect(date.year, `${key} year is not a valid year`).to.be.a('number'); + expect(date.year).to.be.at.least(2024); + expect(date.month, `${key} month is not a valid month`).to.be.a('number'); + expect(date.month).to.be.within(1, 12); + expect(date.day, `${key} day is not a valid day`).to.be.a('number'); + }); + }); + }); +}); diff --git a/test/content/stable.test.js b/test/content/stable.test.js index 3266cb7f47..b5950dbf12 100644 --- a/test/content/stable.test.js +++ b/test/content/stable.test.js @@ -6,12 +6,21 @@ import { } from '../helpers/content.helper'; import t from '../../website/common/script/content/translation'; -import * as stable from '../../website/common/script/content/stable'; -import * as eggs from '../../website/common/script/content/eggs'; -import * as potions from '../../website/common/script/content/hatching-potions'; +import stable from '../../website/common/script/content/stable'; +import eggs from '../../website/common/script/content/eggs'; +import potions from '../../website/common/script/content/hatching-potions'; describe('stable', () => { describe('dropPets', () => { + let clock; + beforeEach(() => { + clock = sinon.useFakeTimers(new Date('2020-05-20')); + }); + + afterEach(() => { + clock.restore(); + }); + it('contains a pet for each drop potion * each drop egg', () => { const numberOfDropPotions = Object.keys(potions.drops).length; const numberOfDropEggs = Object.keys(eggs.drops).length; diff --git a/website/client/src/components/hall/heroes.vue b/website/client/src/components/hall/heroes.vue index 06a12b0384..25721640e9 100644 --- a/website/client/src/components/hall/heroes.vue +++ b/website/client/src/components/hall/heroes.vue @@ -320,7 +320,7 @@