diff --git a/Dockerfile-Dev b/Dockerfile-Dev index b0b07b6dfd..6bc1dc845f 100644 --- a/Dockerfile-Dev +++ b/Dockerfile-Dev @@ -1,18 +1,5 @@ -FROM node:10 - -# Install global packages -RUN npm install -g gulp-cli mocha - -# Clone Habitica repo and install dependencies -RUN mkdir -p /usr/src/habitrpg -WORKDIR /usr/src/habitrpg -RUN git clone https://github.com/HabitRPG/habitica.git /usr/src/habitrpg -RUN cp config.json.example config.json -RUN npm install - -# Create Build dir -RUN mkdir -p ./website/build - -# Start Habitica -EXPOSE 3000 -CMD ["npm", "start"] +FROM node:10 +WORKDIR /code +COPY package*.json /code/ +RUN npm install +RUN npm install -g gulp-cli mocha diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index c523bbcd69..ceacc2c94f 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -1,14 +1,45 @@ version: "3" services: - client: + build: + context: . + dockerfile: ./Dockerfile-Dev + command: ["npm", "run", "client:dev"] + depends_on: + - server environment: - - NODE_ENV=development + - BASE_URL=http://server:3000 + image: habitica + networks: + - habitica + ports: + - "8080:8080" volumes: - - '.:/usr/src/habitrpg' - + - .:/code + - /code/node_modules server: + build: + context: . + dockerfile: ./Dockerfile-Dev + command: ["npm", "start"] + depends_on: + - mongo environment: - - NODE_ENV=development + - NODE_DB_URI=mongodb://mongo/habitrpg + image: habitica + networks: + - habitica + ports: + - "3000:3000" volumes: - - '.:/usr/src/habitrpg' + - .:/code + - /code/node_modules + mongo: + image: mongo:3.4 + networks: + - habitica + ports: + - "27017:27017" +networks: + habitica: + driver: bridge diff --git a/test/api/unit/middlewares/auth.test.js b/test/api/unit/middlewares/auth.test.js index 25ba3d17ea..830af9be04 100644 --- a/test/api/unit/middlewares/auth.test.js +++ b/test/api/unit/middlewares/auth.test.js @@ -16,7 +16,7 @@ describe('auth middleware', () => { describe('auth with headers', () => { it('allows to specify a list of user field that we do not want to load', (done) => { const authWithHeaders = authWithHeadersFactory({ - userFieldsToExclude: ['items', 'flags', 'auth.timestamps'], + userFieldsToExclude: ['items'], }); req.headers['x-api-user'] = user._id; @@ -27,11 +27,34 @@ describe('auth middleware', () => { const userToJSON = res.locals.user.toJSON(); expect(userToJSON.items).to.not.exist; - expect(userToJSON.flags).to.not.exist; - expect(userToJSON.auth.timestamps).to.not.exist; + expect(userToJSON.auth).to.exist; + + done(); + }); + }); + + it('makes sure some fields are always included', (done) => { + const authWithHeaders = authWithHeadersFactory({ + userFieldsToExclude: [ + 'items', 'auth.timestamps', + 'preferences', 'notifications', '_id', 'flags', 'auth', // these are always loaded + ], + }); + + req.headers['x-api-user'] = user._id; + req.headers['x-api-key'] = user.apiToken; + + authWithHeaders(req, res, (err) => { + if (err) return done(err); + + const userToJSON = res.locals.user.toJSON(); + expect(userToJSON.items).to.not.exist; + expect(userToJSON.auth.timestamps).to.exist; expect(userToJSON.auth).to.exist; expect(userToJSON.notifications).to.exist; expect(userToJSON.preferences).to.exist; + expect(userToJSON._id).to.exist; + expect(userToJSON.flags).to.exist; done(); }); diff --git a/test/api/unit/models/group.test.js b/test/api/unit/models/group.test.js index 45a100afd7..7d120c055a 100644 --- a/test/api/unit/models/group.test.js +++ b/test/api/unit/models/group.test.js @@ -1,7 +1,7 @@ import moment from 'moment'; import { v4 as generateUUID } from 'uuid'; import validator from 'validator'; -import { sleep } from '../../../helpers/api-unit.helper'; +import { sleep, translationCheck } from '../../../helpers/api-unit.helper'; import { SPAM_MESSAGE_LIMIT, SPAM_MIN_EXEMPT_CONTRIB_LEVEL, @@ -271,7 +271,16 @@ describe('Group Model', () => { party = await Group.findOne({_id: party._id}); expect(Group.prototype.sendChat).to.be.calledOnce; - expect(Group.prototype.sendChat).to.be.calledWith('`Participating Member attacks Wailing Whale for 5.0 damage.` `Wailing Whale attacks party for 7.5 damage.`'); + expect(Group.prototype.sendChat).to.be.calledWith({ + message: '`Participating Member attacks Wailing Whale for 5.0 damage. Wailing Whale attacks party for 7.5 damage.`', + info: { + bossDamage: '7.5', + quest: 'whale', + type: 'boss_damage', + user: 'Participating Member', + userDamage: '5.0', + }, + }); }); it('applies damage only to participating members of party', async () => { @@ -344,7 +353,10 @@ describe('Group Model', () => { party = await Group.findOne({_id: party._id}); expect(Group.prototype.sendChat).to.be.calledTwice; - expect(Group.prototype.sendChat).to.be.calledWith('`You defeated Wailing Whale! Questing party members receive the rewards of victory.`'); + expect(Group.prototype.sendChat).to.be.calledWith({ + message: '`You defeated Wailing Whale! Questing party members receive the rewards of victory.`', + info: { quest: 'whale', type: 'boss_defeated' }, + }); }); it('calls finishQuest when boss has <= 0 hp', async () => { @@ -387,7 +399,10 @@ describe('Group Model', () => { party = await Group.findOne({_id: party._id}); - expect(Group.prototype.sendChat).to.be.calledWith(quest.boss.rage.effect('en')); + expect(Group.prototype.sendChat).to.be.calledWith({ + message: quest.boss.rage.effect('en'), + info: { quest: 'trex_undead', type: 'boss_rage' }, + }); expect(party.quest.progress.hp).to.eql(383.5); expect(party.quest.progress.rage).to.eql(0); }); @@ -437,7 +452,10 @@ describe('Group Model', () => { party = await Group.findOne({_id: party._id}); - expect(Group.prototype.sendChat).to.be.calledWith(quest.boss.rage.effect('en')); + expect(Group.prototype.sendChat).to.be.calledWith({ + message: quest.boss.rage.effect('en'), + info: { quest: 'lostMasterclasser4', type: 'boss_rage' }, + }); expect(party.quest.progress.rage).to.eql(0); let drainedUser = await User.findById(participatingMember._id); @@ -488,7 +506,15 @@ describe('Group Model', () => { party = await Group.findOne({_id: party._id}); expect(Group.prototype.sendChat).to.be.calledOnce; - expect(Group.prototype.sendChat).to.be.calledWith('`Participating Member found 5 Bars of Soap.`'); + expect(Group.prototype.sendChat).to.be.calledWith({ + message: '`Participating Member found 5 Bars of Soap.`', + info: { + items: { soapBars: 5 }, + quest: 'atom1', + type: 'user_found_items', + user: 'Participating Member', + }, + }); }); it('sends a chat message if no progress is made', async () => { @@ -499,7 +525,15 @@ describe('Group Model', () => { party = await Group.findOne({_id: party._id}); expect(Group.prototype.sendChat).to.be.calledOnce; - expect(Group.prototype.sendChat).to.be.calledWith('`Participating Member found 0 Bars of Soap.`'); + expect(Group.prototype.sendChat).to.be.calledWith({ + message: '`Participating Member found 0 Bars of Soap.`', + info: { + items: { soapBars: 0 }, + quest: 'atom1', + type: 'user_found_items', + user: 'Participating Member', + }, + }); }); it('sends a chat message if no progress is made on quest with multiple items', async () => { @@ -516,9 +550,15 @@ describe('Group Model', () => { party = await Group.findOne({_id: party._id}); expect(Group.prototype.sendChat).to.be.calledOnce; - expect(Group.prototype.sendChat).to.be.calledWithMatch(/`Participating Member found/); - expect(Group.prototype.sendChat).to.be.calledWithMatch(/0 Blue Fins/); - expect(Group.prototype.sendChat).to.be.calledWithMatch(/0 Fire Coral/); + expect(Group.prototype.sendChat).to.be.calledWith({ + message: '`Participating Member found 0 Fire Coral, 0 Blue Fins.`', + info: { + items: { blueFins: 0, fireCoral: 0 }, + quest: 'dilatoryDistress1', + type: 'user_found_items', + user: 'Participating Member', + }, + }); }); it('handles collection quests with multiple items', async () => { @@ -535,8 +575,14 @@ describe('Group Model', () => { party = await Group.findOne({_id: party._id}); expect(Group.prototype.sendChat).to.be.calledOnce; - expect(Group.prototype.sendChat).to.be.calledWithMatch(/`Participating Member found/); - expect(Group.prototype.sendChat).to.be.calledWithMatch(/\d* (Tracks|Broken Twigs)/); + expect(Group.prototype.sendChat).to.be.calledWithMatch({ + message: sinon.match(/`Participating Member found/).and(sinon.match(/\d* (Tracks|Broken Twigs)/)), + info: { + quest: 'evilsanta2', + type: 'user_found_items', + user: 'Participating Member', + }, + }); }); it('sends message about victory', async () => { @@ -547,7 +593,10 @@ describe('Group Model', () => { party = await Group.findOne({_id: party._id}); expect(Group.prototype.sendChat).to.be.calledTwice; - expect(Group.prototype.sendChat).to.be.calledWith('`All items found! Party has received their rewards.`'); + expect(Group.prototype.sendChat).to.be.calledWith({ + message: '`All items found! Party has received their rewards.`', + info: { type: 'all_items_found' }, + }); }); it('calls finishQuest when all items are found', async () => { @@ -718,6 +767,258 @@ describe('Group Model', () => { expect(res.t).to.not.be.called; }); }); + + describe('translateSystemMessages', () => { + it('translate quest_start', async () => { + questLeader.preferences.language = 'en'; + party.chat = [{ + info: { + type: 'quest_start', + quest: 'basilist', + }, + }]; + let toJSON = await Group.toJSONCleanChat(party, questLeader); + translationCheck(toJSON.chat[0].text); + }); + + it('translate boss_damage', async () => { + questLeader.preferences.language = 'en'; + party.chat = [{ + info: { + type: 'boss_damage', + user: questLeader.profile.name, + quest: 'basilist', + userDamage: 15.3, + bossDamage: 3.7, + }, + }]; + let toJSON = await Group.toJSONCleanChat(party, questLeader); + translationCheck(toJSON.chat[0].text); + }); + + it('translate boss_dont_attack', async () => { + questLeader.preferences.language = 'en'; + party.chat = [{ + info: { + type: 'boss_dont_attack', + user: questLeader.profile.name, + quest: 'basilist', + userDamage: 15.3, + }, + }]; + let toJSON = await Group.toJSONCleanChat(party, questLeader); + translationCheck(toJSON.chat[0].text); + }); + + it('translate boss_rage', async () => { + questLeader.preferences.language = 'en'; + party.chat = [{ + info: { + type: 'boss_rage', + quest: 'lostMasterclasser3', + }, + }]; + let toJSON = await Group.toJSONCleanChat(party, questLeader); + translationCheck(toJSON.chat[0].text); + }); + + it('translate boss_defeated', async () => { + questLeader.preferences.language = 'en'; + party.chat = [{ + info: { + type: 'boss_defeated', + quest: 'lostMasterclasser3', + }, + }]; + let toJSON = await Group.toJSONCleanChat(party, questLeader); + translationCheck(toJSON.chat[0].text); + }); + + it('translate user_found_items', async () => { + questLeader.preferences.language = 'en'; + party.chat = [{ + info: { + type: 'user_found_items', + user: questLeader.profile.name, + quest: 'lostMasterclasser1', + items: { + ancientTome: 3, + forbiddenTome: 2, + hiddenTome: 1, + }, + }, + }]; + let toJSON = await Group.toJSONCleanChat(party, questLeader); + translationCheck(toJSON.chat[0].text); + }); + + it('translate all_items_found', async () => { + questLeader.preferences.language = 'en'; + party.chat = [{ + info: { + type: 'all_items_found', + }, + }]; + let toJSON = await Group.toJSONCleanChat(party, questLeader); + translationCheck(toJSON.chat[0].text); + }); + + it('translate spell_cast_party', async () => { + questLeader.preferences.language = 'en'; + party.chat = [{ + info: { + type: 'spell_cast_party', + user: questLeader.profile.name, + class: 'wizard', + spell: 'earth', + }, + }]; + let toJSON = await Group.toJSONCleanChat(party, questLeader); + translationCheck(toJSON.chat[0].text); + }); + + it('translate spell_cast_user', async () => { + questLeader.preferences.language = 'en'; + party.chat = [{ + info: { + type: 'spell_cast_user', + user: questLeader.profile.name, + class: 'special', + spell: 'snowball', + target: participatingMember.profile.name, + }, + }]; + let toJSON = await Group.toJSONCleanChat(party, questLeader); + translationCheck(toJSON.chat[0].text); + }); + + it('translate quest_cancel', async () => { + questLeader.preferences.language = 'en'; + party.chat = [{ + info: { + type: 'quest_cancel', + user: questLeader.profile.name, + quest: 'basilist', + }, + }]; + let toJSON = await Group.toJSONCleanChat(party, questLeader); + translationCheck(toJSON.chat[0].text); + }); + + it('translate quest_abort', async () => { + questLeader.preferences.language = 'en'; + party.chat = [{ + info: { + type: 'quest_abort', + user: questLeader.profile.name, + quest: 'basilist', + }, + }]; + let toJSON = await Group.toJSONCleanChat(party, questLeader); + translationCheck(toJSON.chat[0].text); + }); + + it('translate tavern_quest_completed', async () => { + questLeader.preferences.language = 'en'; + party.chat = [{ + info: { + type: 'tavern_quest_completed', + quest: 'stressbeast', + }, + }]; + let toJSON = await Group.toJSONCleanChat(party, questLeader); + translationCheck(toJSON.chat[0].text); + }); + + it('translate tavern_boss_rage_tired', async () => { + questLeader.preferences.language = 'en'; + party.chat = [{ + info: { + type: 'tavern_boss_rage_tired', + quest: 'stressbeast', + }, + }]; + let toJSON = await Group.toJSONCleanChat(party, questLeader); + translationCheck(toJSON.chat[0].text); + }); + + it('translate tavern_boss_rage', async () => { + questLeader.preferences.language = 'en'; + party.chat = [{ + info: { + type: 'tavern_boss_rage', + quest: 'dysheartener', + scene: 'market', + }, + }]; + let toJSON = await Group.toJSONCleanChat(party, questLeader); + translationCheck(toJSON.chat[0].text); + }); + + it('translate tavern_boss_desperation', async () => { + questLeader.preferences.language = 'en'; + party.chat = [{ + info: { + type: 'tavern_boss_desperation', + quest: 'stressbeast', + }, + }]; + let toJSON = await Group.toJSONCleanChat(party, questLeader); + translationCheck(toJSON.chat[0].text); + }); + + it('translate claim_task', async () => { + questLeader.preferences.language = 'en'; + party.chat = [{ + info: { + type: 'claim_task', + user: questLeader.profile.name, + task: 'Feed the pet', + }, + }]; + let toJSON = await Group.toJSONCleanChat(party, questLeader); + translationCheck(toJSON.chat[0].text); + }); + }); + + describe('toJSONCleanChat', () => { + it('shows messages with 1 flag to non-admins', async () => { + party.chat = [{ + flagCount: 1, + info: { + type: 'quest_start', + quest: 'basilist', + }, + }]; + let toJSON = await Group.toJSONCleanChat(party, questLeader); + expect(toJSON.chat.length).to.equal(1); + }); + + it('shows messages with >= 2 flag to admins', async () => { + party.chat = [{ + flagCount: 3, + info: { + type: 'quest_start', + quest: 'basilist', + }, + }]; + const admin = new User({'contributor.admin': true}); + let toJSON = await Group.toJSONCleanChat(party, admin); + expect(toJSON.chat.length).to.equal(1); + }); + + it('doesn\'t show flagged messages to non-admins', async () => { + party.chat = [{ + flagCount: 3, + info: { + type: 'quest_start', + quest: 'basilist', + }, + }]; + let toJSON = await Group.toJSONCleanChat(party, questLeader); + expect(toJSON.chat.length).to.equal(0); + }); + }); }); context('Instance Methods', () => { @@ -1007,20 +1308,22 @@ describe('Group Model', () => { }); it('formats message', () => { - const chatMessage = party.sendChat('a new message', { - _id: 'user-id', - profile: { name: 'user name' }, - contributor: { - toObject () { - return 'contributor object'; + const chatMessage = party.sendChat({ + message: 'a new message', user: { + _id: 'user-id', + profile: { name: 'user name' }, + contributor: { + toObject () { + return 'contributor object'; + }, }, - }, - backer: { - toObject () { - return 'backer object'; + backer: { + toObject () { + return 'backer object'; + }, }, - }, - }); + }} + ); const chat = chatMessage; @@ -1037,7 +1340,7 @@ describe('Group Model', () => { }); it('formats message as system if no user is passed in', () => { - const chat = party.sendChat('a system message'); + const chat = party.sendChat({message: 'a system message'}); expect(chat.text).to.eql('a system message'); expect(validator.isUUID(chat.id)).to.eql(true); @@ -1052,7 +1355,7 @@ describe('Group Model', () => { }); it('updates users about new messages in party', () => { - party.sendChat('message'); + party.sendChat({message: 'message'}); expect(User.update).to.be.calledOnce; expect(User.update).to.be.calledWithMatch({ @@ -1066,7 +1369,7 @@ describe('Group Model', () => { type: 'guild', }); - group.sendChat('message'); + group.sendChat({message: 'message'}); expect(User.update).to.be.calledOnce; expect(User.update).to.be.calledWithMatch({ @@ -1076,7 +1379,7 @@ describe('Group Model', () => { }); it('does not send update to user that sent the message', () => { - party.sendChat('message', {_id: 'user-id', profile: { name: 'user' }}); + party.sendChat({message: 'message', user: {_id: 'user-id', profile: { name: 'user' }}}); expect(User.update).to.be.calledOnce; expect(User.update).to.be.calledWithMatch({ @@ -1088,7 +1391,7 @@ describe('Group Model', () => { it('skips sending new message notification for guilds with > 5000 members', () => { party.memberCount = 5001; - party.sendChat('message'); + party.sendChat({message: 'message'}); expect(User.update).to.not.be.called; }); @@ -1096,7 +1399,7 @@ describe('Group Model', () => { it('skips sending messages to the tavern', () => { party._id = TAVERN_ID; - party.sendChat('message'); + party.sendChat({message: 'message'}); expect(User.update).to.not.be.called; }); @@ -1928,7 +2231,7 @@ describe('Group Model', () => { await guild.save(); - const groupMessage = guild.sendChat('Test message.'); + const groupMessage = guild.sendChat({message: 'Test message.'}); await groupMessage.save(); await sleep(); diff --git a/test/api/v3/integration/groups/POST-groups.test.js b/test/api/v3/integration/groups/POST-groups.test.js index 9eeb6d0589..5fd322a564 100644 --- a/test/api/v3/integration/groups/POST-groups.test.js +++ b/test/api/v3/integration/groups/POST-groups.test.js @@ -149,7 +149,7 @@ describe('POST /group', () => { ).to.eventually.be.rejected.and.eql({ code: 401, error: 'NotAuthorized', - message: t('cannotCreatePublicGuildWhenMuted'), + message: t('chatPrivilegesRevoked'), }); }); }); diff --git a/test/api/v3/integration/groups/POST-groups_invite.test.js b/test/api/v3/integration/groups/POST-groups_invite.test.js index f5069b3875..2a648e2bb3 100644 --- a/test/api/v3/integration/groups/POST-groups_invite.test.js +++ b/test/api/v3/integration/groups/POST-groups_invite.test.js @@ -100,7 +100,7 @@ describe('Post /groups/:groupId/invite', () => { .to.eventually.be.rejected.and.eql({ code: 401, error: 'NotAuthorized', - message: t('cannotInviteWhenMuted'), + message: t('chatPrivilegesRevoked'), }); }); @@ -262,7 +262,7 @@ describe('Post /groups/:groupId/invite', () => { .to.eventually.be.rejected.and.eql({ code: 401, error: 'NotAuthorized', - message: t('cannotInviteWhenMuted'), + message: t('chatPrivilegesRevoked'), }); }); @@ -436,7 +436,7 @@ describe('Post /groups/:groupId/invite', () => { .to.eventually.be.rejected.and.eql({ code: 401, error: 'NotAuthorized', - message: t('cannotInviteWhenMuted'), + message: t('chatPrivilegesRevoked'), }); }); @@ -526,7 +526,7 @@ describe('Post /groups/:groupId/invite', () => { .to.eventually.be.rejected.and.eql({ code: 401, error: 'NotAuthorized', - message: t('cannotInviteWhenMuted'), + message: t('chatPrivilegesRevoked'), }); }); diff --git a/test/api/v3/integration/quests/POST-groups_groupid_quests_abort.test.js b/test/api/v3/integration/quests/POST-groups_groupid_quests_abort.test.js index 834be404b6..bbfb871a63 100644 --- a/test/api/v3/integration/quests/POST-groups_groupid_quests_abort.test.js +++ b/test/api/v3/integration/quests/POST-groups_groupid_quests_abort.test.js @@ -127,7 +127,13 @@ describe('POST /groups/:groupId/quests/abort', () => { members: {}, }); expect(Group.prototype.sendChat).to.be.calledOnce; - expect(Group.prototype.sendChat).to.be.calledWithMatch(/aborted the party quest Wail of the Whale.`/); + expect(Group.prototype.sendChat).to.be.calledWithMatch({ + message: sinon.match(/aborted the party quest Wail of the Whale.`/), + info: { + quest: 'whale', + type: 'quest_abort', + }, + }); stub.restore(); }); diff --git a/test/api/v3/integration/quests/POST-groups_groupid_quests_cancel.test.js b/test/api/v3/integration/quests/POST-groups_groupid_quests_cancel.test.js index a8095a3cee..4032c36306 100644 --- a/test/api/v3/integration/quests/POST-groups_groupid_quests_cancel.test.js +++ b/test/api/v3/integration/quests/POST-groups_groupid_quests_cancel.test.js @@ -141,7 +141,14 @@ describe('POST /groups/:groupId/quests/cancel', () => { members: {}, }); expect(Group.prototype.sendChat).to.be.calledOnce; - expect(Group.prototype.sendChat).to.be.calledWithMatch(/cancelled the party quest Wail of the Whale.`/); + expect(Group.prototype.sendChat).to.be.calledWithMatch({ + message: sinon.match(/cancelled the party quest Wail of the Whale.`/), + info: { + quest: 'whale', + type: 'quest_cancel', + user: sinon.match.any, + }, + }); stub.restore(); }); diff --git a/test/helpers/api-unit.helper.js b/test/helpers/api-unit.helper.js index 9b8d13d420..b43389bf21 100644 --- a/test/helpers/api-unit.helper.js +++ b/test/helpers/api-unit.helper.js @@ -8,6 +8,7 @@ import mongo from './mongo'; // eslint-disable-line import moment from 'moment'; import i18n from '../../website/common/script/i18n'; import * as Tasks from '../../website/server/models/task'; +export { translationCheck } from './translate'; afterEach((done) => { sandbox.restore(); diff --git a/test/helpers/translate.js b/test/helpers/translate.js index e146c27a71..c12e97c147 100644 --- a/test/helpers/translate.js +++ b/test/helpers/translate.js @@ -16,3 +16,9 @@ export function translate (key, variables, language) { return translatedString; } + +export function translationCheck (translatedString) { + expect(translatedString).to.not.be.empty; + expect(translatedString).to.not.eql(STRING_ERROR_MSG); + expect(translatedString).to.not.match(STRING_DOES_NOT_EXIST_MSG); +} diff --git a/website/client/components/groups/questSidebarSection.vue b/website/client/components/groups/questSidebarSection.vue index 8c09ee5c94..66ed19ae56 100644 --- a/website/client/components/groups/questSidebarSection.vue +++ b/website/client/components/groups/questSidebarSection.vue @@ -35,7 +35,7 @@ sidebar-section(:title="$t('questDetailsTitle')") .grey-progress-bar .collect-progress-bar(:style="{width: (group.quest.progress.collect[key] / value.count) * 100 + '%'}") strong {{group.quest.progress.collect[key]}} / {{value.count}} - div.text-right {{parseFloat(user.party.quest.progress.collectedItems) || 0}} items found + div.text-right(v-if='userIsOnQuest') {{parseFloat(user.party.quest.progress.collectedItems) || 0}} items found .boss-info(v-if='questData.boss') .row .col-6 diff --git a/website/client/components/header/menu.vue b/website/client/components/header/menu.vue index 169a599a36..336cbd6f71 100644 --- a/website/client/components/header/menu.vue +++ b/website/client/components/header/menu.vue @@ -6,12 +6,12 @@ div report-flag-modal send-gems-modal b-navbar.topbar.navbar-inverse.static-top(toggleable="lg", type="dark", :class="navbarZIndexClass") - b-navbar-brand.brand + b-navbar-brand.brand(aria-label="Habitica") .logo.svg-icon.d-none.d-xl-block(v-html="icons.logo") .svg-icon.gryphon.d-xs-block.d-xl-none b-navbar-toggle(target='menu_collapse').menu-toggle .quick-menu.mobile-only.form-inline - a.item-with-icon(@click="sync", v-b-tooltip.hover.bottom="$t('sync')") + a.item-with-icon(@click="sync", v-b-tooltip.hover.bottom="$t('sync')", :aria-label="$t('sync')") .top-menu-icon.svg-icon(v-html="icons.sync") notification-menu.item-with-icon user-dropdown.item-with-icon @@ -64,13 +64,13 @@ div .top-menu-icon.svg-icon(v-html="icons.hourglasses", v-b-tooltip.hover.bottom="$t('mysticHourglassesTooltip')") span {{ userHourglasses }} .item-with-icon - .top-menu-icon.svg-icon.gem(v-html="icons.gem", @click='showBuyGemsModal("gems")', v-b-tooltip.hover.bottom="$t('gems')") + a.top-menu-icon.svg-icon.gem(:aria-label="$t('gems')", href="#buy-gems" v-html="icons.gem", @click.prevent='showBuyGemsModal("gems")', v-b-tooltip.hover.bottom="$t('gems')") span {{userGems}} .item-with-icon.gold - .top-menu-icon.svg-icon(v-html="icons.gold", v-b-tooltip.hover.bottom="$t('gold')") + .top-menu-icon.svg-icon(:aria-label="$t('gold')", v-html="icons.gold", v-b-tooltip.hover.bottom="$t('gold')") span {{Math.floor(user.stats.gp * 100) / 100}} .form-inline.desktop-only - a.item-with-icon(@click="sync", v-b-tooltip.hover.bottom="$t('sync')") + a.item-with-icon(@click="sync", @keyup.enter="sync", role="link", :aria-label="$t('sync')", tabindex="0", v-b-tooltip.hover.bottom="$t('sync')") .top-menu-icon.svg-icon(v-html="icons.sync") notification-menu.item-with-icon user-dropdown.item-with-icon @@ -290,6 +290,7 @@ div margin-right: 24px; } + &:focus /deep/ .top-menu-icon.svg-icon, &:hover /deep/ .top-menu-icon.svg-icon { color: $white; } diff --git a/website/client/components/header/notificationsDropdown.vue b/website/client/components/header/notificationsDropdown.vue index 48f7bba6e7..6fce4cc0cf 100644 --- a/website/client/components/header/notificationsDropdown.vue +++ b/website/client/components/header/notificationsDropdown.vue @@ -1,7 +1,7 @@