`;
+
+exports[`Admin/BadgesSection without badges renders 1`] = `
+
+
+
+
+
+
+
+
+
+ admin.badges.noBadges
+
+
+
+
+
+`;
diff --git a/webapp/config/index.js b/webapp/config/index.js
index 5da17010b..fb275a8ec 100644
--- a/webapp/config/index.js
+++ b/webapp/config/index.js
@@ -35,6 +35,7 @@ const options = {
COOKIE_EXPIRE_TIME: process.env.COOKIE_EXPIRE_TIME || 730, // Two years by default
COOKIE_HTTPS_ONLY: process.env.COOKIE_HTTPS_ONLY || process.env.NODE_ENV === 'production', // ensure true in production if not set explicitly
CATEGORIES_ACTIVE: process.env.CATEGORIES_ACTIVE === 'true' || false,
+ BADGES_ENABLED: process.env.BADGES_ENABLED === 'true' || false,
}
const CONFIG = {
diff --git a/webapp/graphql/Fragments.js b/webapp/graphql/Fragments.js
index d0ad8a0fe..77af830e8 100644
--- a/webapp/graphql/Fragments.js
+++ b/webapp/graphql/Fragments.js
@@ -26,9 +26,15 @@ export const locationFragment = (lang) => gql`
export const badgesFragment = gql`
fragment badges on User {
- badgeTrophies {
+ badgeTrophiesSelected {
id
icon
+ description
+ }
+ badgeVerification {
+ id
+ icon
+ description
}
}
`
diff --git a/webapp/graphql/User.js b/webapp/graphql/User.js
index 147e93c6f..75342ef2a 100644
--- a/webapp/graphql/User.js
+++ b/webapp/graphql/User.js
@@ -405,6 +405,22 @@ export const currentUserQuery = gql`
query {
currentUser {
...user
+ badgeTrophiesSelected {
+ id
+ icon
+ description
+ isDefault
+ }
+ badgeTrophiesUnused {
+ id
+ icon
+ description
+ }
+ badgeVerification {
+ id
+ icon
+ description
+ }
email
role
about
@@ -466,3 +482,43 @@ export const userDataQuery = (i18n) => {
}
`
}
+
+export const setTrophyBadgeSelected = gql`
+ mutation ($slot: Int!, $badgeId: ID) {
+ setTrophyBadgeSelected(slot: $slot, badgeId: $badgeId) {
+ badgeTrophiesCount
+ badgeTrophiesSelected {
+ id
+ icon
+ description
+ isDefault
+ }
+ badgeTrophiesUnused {
+ id
+ icon
+ description
+ }
+ badgeTrophiesUnusedCount
+ }
+ }
+`
+
+export const resetTrophyBadgesSelected = gql`
+ mutation {
+ resetTrophyBadgesSelected {
+ badgeTrophiesCount
+ badgeTrophiesSelected {
+ id
+ icon
+ description
+ isDefault
+ }
+ badgeTrophiesUnused {
+ id
+ icon
+ description
+ }
+ badgeTrophiesUnusedCount
+ }
+ }
+`
diff --git a/webapp/locales/de.json b/webapp/locales/de.json
index 21590e556..71e792aa0 100644
--- a/webapp/locales/de.json
+++ b/webapp/locales/de.json
@@ -12,6 +12,7 @@
"admin": {
"badges": {
"description": "Stelle die verfügbaren Auszeichnungen für diesen Nutzer ein.",
+ "noBadges": "Keine Auszeichnungen vorhanden.",
"revokeTrophy": {
"error": "Trophäe konnte nicht widerrufen werden!",
"success": "Trophäe erfolgreich widerrufen"
@@ -97,8 +98,7 @@
"number": "Nr.",
"role": "Rolle",
"slug": "Alias"
- },
- "edit": "Bearbeiten"
+ }
}
}
},
@@ -957,6 +957,16 @@
"title": "Suchergebnisse"
},
"settings": {
+ "badges": {
+ "click-to-select": "Klicke auf einen freien Platz, um eine Badge hinzufügen.",
+ "click-to-use": "Klicke auf eine Badge, um sie zu platzieren.",
+ "description": "Hier hast du die Möglichkeit zu entscheiden, wie deine bereits erworbenen Badges in deinem Profil gezeigt werden sollen.",
+ "name": "Badges",
+ "no-badges-available": "Im Moment stehen dir keine Badges zur Verfügung, die du hinzufügen könntest.",
+ "remove": "Badge entfernen",
+ "success-update": "Deine Badges wurden erfolgreich gespeichert.",
+ "verification": "Dies ist deine Verifikations-Badge und kann nicht geändert werden."
+ },
"blocked-users": {
"block": "Nutzer blockieren",
"columns": {
diff --git a/webapp/locales/en.json b/webapp/locales/en.json
index 8d270349e..6b0de56c9 100644
--- a/webapp/locales/en.json
+++ b/webapp/locales/en.json
@@ -12,6 +12,7 @@
"admin": {
"badges": {
"description": "Configure the available badges for this user",
+ "noBadges": "There are no badges available",
"revokeTrophy": {
"error": "Trophy could not be revoked!",
"success": "Trophy successfully revoked!"
@@ -97,8 +98,7 @@
"number": "No.",
"role": "Role",
"slug": "Slug"
- },
- "edit": "Edit"
+ }
}
}
},
@@ -957,6 +957,16 @@
"title": "Search Results"
},
"settings": {
+ "badges": {
+ "click-to-select": "Click on an empty space to add a badge.",
+ "click-to-use": "Click on a badge to use it in the selected slot.",
+ "description": "Here you can choose how to display your earned badges on your profile.",
+ "name": "Badges",
+ "no-badges-available": "You currently don't have any badges available to add.",
+ "remove": "Remove Badge",
+ "success-update": "Your badges have been updated successfully.",
+ "verification": "This is your verification badge and cannot be changed."
+ },
"blocked-users": {
"block": "Block user",
"columns": {
diff --git a/webapp/locales/es.json b/webapp/locales/es.json
index 31f2cc5f4..15096b9d8 100644
--- a/webapp/locales/es.json
+++ b/webapp/locales/es.json
@@ -12,6 +12,7 @@
"admin": {
"badges": {
"description": null,
+ "noBadges": null,
"revokeTrophy": {
"error": null,
"success": null
@@ -97,8 +98,7 @@
"number": "No.",
"role": "Rol",
"slug": "Alias"
- },
- "edit": null
+ }
}
}
},
@@ -957,6 +957,16 @@
"title": null
},
"settings": {
+ "badges": {
+ "click-to-select": null,
+ "click-to-use": null,
+ "description": null,
+ "name": null,
+ "no-badges-available": null,
+ "remove": null,
+ "success-update": null,
+ "verification": null
+ },
"blocked-users": {
"block": "Bloquear usuario",
"columns": {
diff --git a/webapp/locales/fr.json b/webapp/locales/fr.json
index 4bbca2b82..2da2a9801 100644
--- a/webapp/locales/fr.json
+++ b/webapp/locales/fr.json
@@ -12,6 +12,7 @@
"admin": {
"badges": {
"description": null,
+ "noBadges": null,
"revokeTrophy": {
"error": null,
"success": null
@@ -97,8 +98,7 @@
"number": "Num.",
"role": "Rôle",
"slug": "Slug"
- },
- "edit": null
+ }
}
}
},
@@ -957,6 +957,16 @@
"title": null
},
"settings": {
+ "badges": {
+ "click-to-select": null,
+ "click-to-use": null,
+ "description": null,
+ "name": null,
+ "no-badges-available": null,
+ "remove": null,
+ "success-update": null,
+ "verification": null
+ },
"blocked-users": {
"block": "Bloquer l'utilisateur",
"columns": {
diff --git a/webapp/locales/it.json b/webapp/locales/it.json
index 21bfaa859..485abff3a 100644
--- a/webapp/locales/it.json
+++ b/webapp/locales/it.json
@@ -12,6 +12,7 @@
"admin": {
"badges": {
"description": null,
+ "noBadges": null,
"revokeTrophy": {
"error": null,
"success": null
@@ -97,8 +98,7 @@
"number": null,
"role": null,
"slug": null
- },
- "edit": null
+ }
}
}
},
@@ -957,6 +957,16 @@
"title": null
},
"settings": {
+ "badges": {
+ "click-to-select": null,
+ "click-to-use": null,
+ "description": null,
+ "name": null,
+ "no-badges-available": null,
+ "remove": null,
+ "success-update": null,
+ "verification": null
+ },
"blocked-users": {
"block": null,
"columns": {
diff --git a/webapp/locales/nl.json b/webapp/locales/nl.json
index f67518c21..40f9aca2e 100644
--- a/webapp/locales/nl.json
+++ b/webapp/locales/nl.json
@@ -12,6 +12,7 @@
"admin": {
"badges": {
"description": null,
+ "noBadges": null,
"revokeTrophy": {
"error": null,
"success": null
@@ -97,8 +98,7 @@
"number": null,
"role": null,
"slug": null
- },
- "edit": null
+ }
}
}
},
@@ -957,6 +957,16 @@
"title": null
},
"settings": {
+ "badges": {
+ "click-to-select": null,
+ "click-to-use": null,
+ "description": null,
+ "name": null,
+ "no-badges-available": null,
+ "remove": null,
+ "success-update": null,
+ "verification": null
+ },
"blocked-users": {
"block": null,
"columns": {
diff --git a/webapp/locales/pl.json b/webapp/locales/pl.json
index 4c6a96a5f..ee332b84b 100644
--- a/webapp/locales/pl.json
+++ b/webapp/locales/pl.json
@@ -12,6 +12,7 @@
"admin": {
"badges": {
"description": null,
+ "noBadges": null,
"revokeTrophy": {
"error": null,
"success": null
@@ -97,8 +98,7 @@
"number": null,
"role": null,
"slug": null
- },
- "edit": null
+ }
}
}
},
@@ -957,6 +957,16 @@
"title": null
},
"settings": {
+ "badges": {
+ "click-to-select": null,
+ "click-to-use": null,
+ "description": null,
+ "name": null,
+ "no-badges-available": null,
+ "remove": null,
+ "success-update": null,
+ "verification": null
+ },
"blocked-users": {
"block": null,
"columns": {
diff --git a/webapp/locales/pt.json b/webapp/locales/pt.json
index 7d5ad52c1..54f9b5d99 100644
--- a/webapp/locales/pt.json
+++ b/webapp/locales/pt.json
@@ -12,6 +12,7 @@
"admin": {
"badges": {
"description": null,
+ "noBadges": null,
"revokeTrophy": {
"error": null,
"success": null
@@ -97,8 +98,7 @@
"number": "N.º",
"role": "Função",
"slug": "Slug"
- },
- "edit": null
+ }
}
}
},
@@ -957,6 +957,16 @@
"title": null
},
"settings": {
+ "badges": {
+ "click-to-select": null,
+ "click-to-use": null,
+ "description": null,
+ "name": null,
+ "no-badges-available": null,
+ "remove": null,
+ "success-update": null,
+ "verification": null
+ },
"blocked-users": {
"block": "Bloquear usuário",
"columns": {
diff --git a/webapp/locales/ru.json b/webapp/locales/ru.json
index 3a394d6ff..4d2e2a357 100644
--- a/webapp/locales/ru.json
+++ b/webapp/locales/ru.json
@@ -12,6 +12,7 @@
"admin": {
"badges": {
"description": null,
+ "noBadges": null,
"revokeTrophy": {
"error": null,
"success": null
@@ -97,8 +98,7 @@
"number": "№",
"role": "Роль",
"slug": "Алиас"
- },
- "edit": null
+ }
}
}
},
@@ -957,6 +957,16 @@
"title": null
},
"settings": {
+ "badges": {
+ "click-to-select": null,
+ "click-to-use": null,
+ "description": null,
+ "name": null,
+ "no-badges-available": null,
+ "remove": null,
+ "success-update": null,
+ "verification": null
+ },
"blocked-users": {
"block": "Блокировать",
"columns": {
diff --git a/webapp/maintenance/source/package.json b/webapp/maintenance/source/package.json
index 1aa029eb4..df76a77ca 100644
--- a/webapp/maintenance/source/package.json
+++ b/webapp/maintenance/source/package.json
@@ -1,6 +1,6 @@
{
"name": "@ocelot-social/maintenance",
- "version": "3.3.0",
+ "version": "3.4.0",
"description": "Maintenance page for ocelot.social",
"repository": "https://github.com/Ocelot-Social-Community/Ocelot-Social",
"author": "ocelot.social Community",
diff --git a/webapp/nuxt.config.js b/webapp/nuxt.config.js
index 07cfa6bc4..263c3f149 100644
--- a/webapp/nuxt.config.js
+++ b/webapp/nuxt.config.js
@@ -207,15 +207,6 @@ export default {
'X-API-TOKEN': CONFIG.BACKEND_TOKEN,
},
},
- '/img': {
- // make this configurable (nuxt-dotenv)
- target: CONFIG.GRAPHQL_URI,
- toProxy: true, // cloudflare needs that
- headers: {
- 'X-UI-Request': true,
- 'X-API-TOKEN': CONFIG.BACKEND_TOKEN,
- },
- },
},
// Give apollo module options
diff --git a/webapp/package.json b/webapp/package.json
index d18a88408..574c94378 100644
--- a/webapp/package.json
+++ b/webapp/package.json
@@ -1,6 +1,6 @@
{
"name": "ocelot-social-webapp",
- "version": "3.3.0",
+ "version": "3.4.0",
"description": "ocelot.social Frontend",
"repository": "https://github.com/Ocelot-Social-Community/Ocelot-Social",
"author": "ocelot.social Community",
@@ -79,6 +79,7 @@
"@storybook/addon-actions": "^5.3.21",
"@storybook/addon-notes": "^5.3.18",
"@storybook/vue": "~7.4.0",
+ "@testing-library/jest-dom": "^6.6.3",
"@testing-library/vue": "5",
"@vue/cli-shared-utils": "~4.3.1",
"@vue/eslint-config-prettier": "~6.0.0",
diff --git a/webapp/pages/__snapshots__/settings.spec.js.snap b/webapp/pages/__snapshots__/settings.spec.js.snap
new file mode 100644
index 000000000..6672e41af
--- /dev/null
+++ b/webapp/pages/__snapshots__/settings.spec.js.snap
@@ -0,0 +1,427 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`settings.vue given badges are disabled renders 1`] = `
+
+`;
+
+exports[`settings.vue given badges are enabled renders 1`] = `
+
+`;
diff --git a/webapp/pages/admin/users/__snapshots__/index.spec.js.snap b/webapp/pages/admin/users/__snapshots__/index.spec.js.snap
new file mode 100644
index 000000000..0fff4016b
--- /dev/null
+++ b/webapp/pages/admin/users/__snapshots__/index.spec.js.snap
@@ -0,0 +1,867 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Users given badges are disabled renders 1`] = `
+
+
+
+ admin.users.name
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ admin.users.table.columns.number
+
+
+
+
+ admin.users.table.columns.name
+
+
+
+
+ admin.users.table.columns.email
+
+
+
+
+ admin.users.table.columns.slug
+
+
+
+
+ admin.users.table.columns.createdAt
+
+
+
+
+ 🖉
+
+
+
+
+ 🗨
+
+
+
+
+ ❤
+
+
+
+
+ admin.users.table.columns.role
+
+
+
+
+
+
+
+ NaN.
+
+
+
+
+ User
+
+
+
+
+
+
+ user@example.org
+
+
+
+
+
+
+ user
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ NaN.
+
+
+
+
+ User
+
+
+
+
+
+
+ user2@example.org
+
+
+
+
+
+
+ user
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Users given badges are enabled renders 1`] = `
+
+
+
+ admin.users.name
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ admin.users.table.columns.number
+
+
+
+
+ admin.users.table.columns.name
+
+
+
+
+ admin.users.table.columns.email
+
+
+
+
+ admin.users.table.columns.slug
+
+
+
+
+ admin.users.table.columns.createdAt
+
+
+
+
+ 🖉
+
+
+
+
+ 🗨
+
+
+
+
+ ❤
+
+
+
+
+ admin.users.table.columns.role
+
+
+
+
+ admin.users.table.columns.badges
+
+
+
+
+
+
+
+ NaN.
+
+
+
+
+ User
+
+
+
+
+
+
+ user@example.org
+
+
+
+
+
+
+ user
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ NaN.
+
+
+
+
+ User
+
+
+
+
+
+
+ user2@example.org
+
+
+
+
+
+
+ user
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/webapp/pages/admin/users/_id.spec.js b/webapp/pages/admin/users/_id.spec.js
index 933de58de..d38b13022 100644
--- a/webapp/pages/admin/users/_id.spec.js
+++ b/webapp/pages/admin/users/_id.spec.js
@@ -58,6 +58,11 @@ describe('.vue', () => {
query: jest.fn(),
},
mutate: jest.fn(),
+ queries: {
+ Badge: {
+ loading: false,
+ },
+ },
},
$toast: {
success: jest.fn(),
diff --git a/webapp/pages/admin/users/_id.vue b/webapp/pages/admin/users/_id.vue
index 808e1653a..a6c4dafaa 100644
--- a/webapp/pages/admin/users/_id.vue
+++ b/webapp/pages/admin/users/_id.vue
@@ -8,7 +8,7 @@
{{ $t('admin.badges.description') }}
-
+
userBadge.id === badge.id),
}))
},
+ isLoadingBadges() {
+ return this.$apollo.queries.Badge.loading
+ },
},
methods: {
toggleBadge(badge) {
diff --git a/webapp/pages/admin/users/index.spec.js b/webapp/pages/admin/users/index.spec.js
index 8d6b923c5..85e8789b8 100644
--- a/webapp/pages/admin/users/index.spec.js
+++ b/webapp/pages/admin/users/index.spec.js
@@ -10,11 +10,9 @@ const stubs = {
describe('Users', () => {
let wrapper
- let Wrapper
- let getters
const mocks = {
- $t: jest.fn(),
+ $t: jest.fn((t) => t),
$apollo: {
loading: false,
mutate: jest
@@ -38,116 +36,154 @@ describe('Users', () => {
},
}
- describe('mount', () => {
- getters = {
- 'auth/isAdmin': () => true,
- 'auth/user': () => {
- return { id: 'admin' }
- },
- }
+ const getters = {
+ 'auth/isAdmin': () => true,
+ 'auth/user': () => {
+ return { id: 'admin' }
+ },
+ }
- Wrapper = () => {
- const store = new Vuex.Store({ getters })
- return mount(Users, {
- mocks,
- localVue,
- store,
- stubs,
- })
- }
+ const Wrapper = () => {
+ const store = new Vuex.Store({ getters })
+ return mount(Users, {
+ mocks,
+ localVue,
+ store,
+ stubs,
+ data: () => ({
+ User: [
+ {
+ id: 'user',
+ email: 'user@example.org',
+ name: 'User',
+ role: 'moderator',
+ slug: 'user',
+ },
+ {
+ id: 'user2',
+ email: 'user2@example.org',
+ name: 'User',
+ role: 'moderator',
+ slug: 'user',
+ },
+ ],
+ }),
+ })
+ }
+
+ describe('given badges are enabled', () => {
+ beforeEach(() => {
+ mocks.$env = {
+ BADGES_ENABLED: true,
+ }
+ wrapper = Wrapper()
+ })
it('renders', () => {
+ expect(wrapper.element).toMatchSnapshot()
+ })
+ })
+
+ describe('given badges are disabled', () => {
+ beforeEach(() => {
+ mocks.$env = {
+ BADGES_ENABLED: false,
+ }
wrapper = Wrapper()
- expect(wrapper.element.tagName).toBe('DIV')
})
- describe('search', () => {
- let searchAction
- beforeEach(() => {
- searchAction = (wrapper, { query }) => {
- wrapper.find('input').setValue(query)
- wrapper.find('form').trigger('submit')
- return wrapper
- }
+ it('renders', () => {
+ expect(wrapper.element).toMatchSnapshot()
+ })
+ })
+
+ describe('search', () => {
+ let searchAction
+ beforeEach(() => {
+ wrapper = Wrapper()
+ searchAction = (wrapper, { query }) => {
+ wrapper.find('input').setValue(query)
+ wrapper.find('form').trigger('submit')
+ return wrapper
+ }
+ })
+
+ describe('query looks like an email address', () => {
+ it('searches users for exact email address', async () => {
+ const wrapper = await searchAction(Wrapper(), { query: 'email@example.org' })
+ expect(wrapper.vm.email).toEqual('email@example.org')
+ expect(wrapper.vm.filter).toBe(null)
})
- describe('query looks like an email address', () => {
- it('searches users for exact email address', async () => {
- const wrapper = await searchAction(Wrapper(), { query: 'email@example.org' })
- expect(wrapper.vm.email).toEqual('email@example.org')
- expect(wrapper.vm.filter).toBe(null)
- })
-
- it('email address is case-insensitive', async () => {
- const wrapper = await searchAction(Wrapper(), { query: 'eMaiL@example.org' })
- expect(wrapper.vm.email).toEqual('email@example.org')
- expect(wrapper.vm.filter).toBe(null)
- })
- })
-
- describe('query is just text', () => {
- it('tries to find matching users by `name`, `slug` or `about`', async () => {
- const wrapper = await searchAction(await Wrapper(), { query: 'Find me' })
- const expected = {
- OR: [
- { name_contains: 'Find me' },
- { slug_contains: 'Find me' },
- { about_contains: 'Find me' },
- ],
- }
- expect(wrapper.vm.email).toBe(null)
- expect(wrapper.vm.filter).toEqual(expected)
- })
+ it('email address is case-insensitive', async () => {
+ const wrapper = await searchAction(Wrapper(), { query: 'eMaiL@example.org' })
+ expect(wrapper.vm.email).toEqual('email@example.org')
+ expect(wrapper.vm.filter).toBe(null)
})
})
- describe('change roles', () => {
- beforeAll(() => {
- wrapper = Wrapper()
- wrapper.setData({
- User: [
- {
- id: 'admin',
- email: 'admin@example.org',
- name: 'Admin',
- role: 'admin',
- slug: 'admin',
- },
- {
- id: 'user',
- email: 'user@example.org',
- name: 'User',
- role: 'user',
- slug: 'user',
- },
+ describe('query is just text', () => {
+ it('tries to find matching users by `name`, `slug` or `about`', async () => {
+ const wrapper = await searchAction(await Wrapper(), { query: 'Find me' })
+ const expected = {
+ OR: [
+ { name_contains: 'Find me' },
+ { slug_contains: 'Find me' },
+ { about_contains: 'Find me' },
],
- userRoles: ['user', 'moderator', 'admin'],
- })
- })
-
- it('cannot change own role', () => {
- const adminRow = wrapper.findAll('tr').at(1)
- expect(adminRow.find('select').exists()).toBe(false)
- })
-
- it('changes the role of another user', () => {
- const userRow = wrapper.findAll('tr').at(2)
- userRow.findAll('option').at(1).setSelected()
- expect(mocks.$apollo.mutate).toHaveBeenCalledWith(
- expect.objectContaining({
- variables: {
- id: 'user',
- role: 'moderator',
- },
- }),
- )
- })
-
- it('toasts a success message after role has changed', () => {
- const userRow = wrapper.findAll('tr').at(2)
- userRow.findAll('option').at(1).setSelected()
- expect(mocks.$toast.success).toHaveBeenCalled()
+ }
+ expect(wrapper.vm.email).toBe(null)
+ expect(wrapper.vm.filter).toEqual(expected)
})
})
})
+
+ describe('change roles', () => {
+ beforeAll(() => {
+ wrapper = Wrapper()
+ wrapper.setData({
+ User: [
+ {
+ id: 'admin',
+ email: 'admin@example.org',
+ name: 'Admin',
+ role: 'admin',
+ slug: 'admin',
+ },
+ {
+ id: 'user',
+ email: 'user@example.org',
+ name: 'User',
+ role: 'user',
+ slug: 'user',
+ },
+ ],
+ userRoles: ['user', 'moderator', 'admin'],
+ })
+ })
+
+ it('cannot change own role', () => {
+ const adminRow = wrapper.findAll('tr').at(1)
+ expect(adminRow.find('select').exists()).toBe(false)
+ })
+
+ it('changes the role of another user', () => {
+ const userRow = wrapper.findAll('tr').at(2)
+ userRow.findAll('option').at(1).setSelected()
+ expect(mocks.$apollo.mutate).toHaveBeenCalledWith(
+ expect.objectContaining({
+ variables: {
+ id: 'user',
+ role: 'moderator',
+ },
+ }),
+ )
+ })
+
+ it('toasts a success message after role has changed', () => {
+ const userRow = wrapper.findAll('tr').at(2)
+ userRow.findAll('option').at(1).setSelected()
+ expect(mocks.$toast.success).toHaveBeenCalled()
+ })
+ })
})
diff --git a/webapp/pages/admin/users/index.vue b/webapp/pages/admin/users/index.vue
index 24258a57f..0bd592bad 100644
--- a/webapp/pages/admin/users/index.vue
+++ b/webapp/pages/admin/users/index.vue
@@ -70,7 +70,7 @@
params: { id: scope.row.id },
}"
>
- {{ $t('admin.users.table.edit') }}
+
@@ -120,7 +120,7 @@ export default {
currentUser: 'auth/user',
}),
fields() {
- return {
+ const fields = {
index: this.$t('admin.users.table.columns.number'),
name: this.$t('admin.users.table.columns.name'),
email: this.$t('admin.users.table.columns.email'),
@@ -142,11 +142,16 @@ export default {
label: this.$t('admin.users.table.columns.role'),
align: 'right',
},
- badges: {
+ }
+
+ if (this.$env.BADGES_ENABLED) {
+ fields.badges = {
label: this.$t('admin.users.table.columns.badges'),
align: 'right',
- },
+ }
}
+
+ return fields
},
},
apollo: {
@@ -219,4 +224,8 @@ export default {
.admin-users > .base-card:first-child {
margin-bottom: $space-small;
}
+
+.ds-table-col {
+ vertical-align: middle;
+}
diff --git a/webapp/pages/profile/_id/__snapshots__/_slug.spec.js.snap b/webapp/pages/profile/_id/__snapshots__/_slug.spec.js.snap
new file mode 100644
index 000000000..9eec4e96a
--- /dev/null
+++ b/webapp/pages/profile/_id/__snapshots__/_slug.spec.js.snap
@@ -0,0 +1,2442 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ProfileSlug given an authenticated user given another profile user and badges are disabled renders 1`] = `
+
+
+
+
+
+
+
+
+
+ BTB
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Bob the builder
+
+
+
+
+
+ @undefined
+
+
+
+
+
+
+
+ profile.memberSince
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
+
+
+
+
+
+
+ profile.followers
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
+
+
+
+
+
+
+ profile.following
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ followButton.follow
+
+
+
+
+
+
+
+
+
+
+ chat.userProfileButton.label
+
+
+
+
+
+
+
+
+
+
+
+
+
+ profile.network.title
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ No results :(
+
+
+
+ No more data :)
+
+
+
+
+ Opps, something went wrong :(
+
+
+
+
+ Retry
+
+
+
+
+
+
+
+
+`;
+
+exports[`ProfileSlug given an authenticated user given another profile user and badges are enabled renders 1`] = `
+
+
+
+
+
+
+
+
+
+ BTB
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Bob the builder
+
+
+
+
+
+ @undefined
+
+
+
+
+
+
+
+ profile.memberSince
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
+
+
+
+
+
+
+ profile.followers
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
+
+
+
+
+
+
+ profile.following
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ followButton.follow
+
+
+
+
+
+
+
+
+
+
+ chat.userProfileButton.label
+
+
+
+
+
+
+
+
+
+
+
+
+
+ profile.network.title
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ No results :(
+
+
+
+ No more data :)
+
+
+
+
+ Opps, something went wrong :(
+
+
+
+
+ Retry
+
+
+
+
+
+
+
+
+`;
+
+exports[`ProfileSlug given an authenticated user given the logged in user as profile user and badges are disabled renders 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ BTB
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Bob the builder
+
+
+
+
+
+ @undefined
+
+
+
+
+
+
+
+ profile.memberSince
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
+
+
+
+
+
+
+ profile.followers
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
+
+
+
+
+
+
+ profile.following
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ profile.network.title
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ No results :(
+
+
+
+ No more data :)
+
+
+
+
+ Opps, something went wrong :(
+
+
+
+
+ Retry
+
+
+
+
+
+
+
+
+`;
+
+exports[`ProfileSlug given an authenticated user given the logged in user as profile user and badges are enabled renders 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ BTB
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Bob the builder
+
+
+
+
+
+ @undefined
+
+
+
+
+
+
+
+ profile.memberSince
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
+
+
+
+
+
+
+ profile.followers
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
+
+
+
+
+
+
+ profile.following
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ profile.network.title
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ No results :(
+
+
+
+ No more data :)
+
+
+
+
+ Opps, something went wrong :(
+
+
+
+
+ Retry
+
+
+
+
+
+
+
+
+`;
diff --git a/webapp/pages/profile/_id/_slug.spec.js b/webapp/pages/profile/_id/_slug.spec.js
index 5ab87ad3a..a4cc473c3 100644
--- a/webapp/pages/profile/_id/_slug.spec.js
+++ b/webapp/pages/profile/_id/_slug.spec.js
@@ -1,22 +1,25 @@
-import { mount } from '@vue/test-utils'
+import { render } from '@testing-library/vue'
import ProfileSlug from './_slug.vue'
const localVue = global.localVue
localVue.filter('date', (d) => d)
+// Mock Math.random, used in Dropdown
+Object.assign(Math, {
+ random: () => 0,
+})
+
const stubs = {
'client-only': true,
'v-popover': true,
'nuxt-link': true,
- 'infinite-loading': true,
'follow-list': true,
'router-link': true,
}
describe('ProfileSlug', () => {
let wrapper
- let Wrapper
let mocks
beforeEach(() => {
@@ -25,7 +28,7 @@ describe('ProfileSlug', () => {
id: 'p23',
name: 'It is a post',
},
- $t: jest.fn(),
+ $t: jest.fn((t) => t),
// If you're mocking router, then don't use VueRouter with localVue: https://vue-test-utils.vuejs.org/guides/using-with-vue-router.html
$route: {
params: {
@@ -49,49 +52,144 @@ describe('ProfileSlug', () => {
}
})
- describe('mount', () => {
- Wrapper = () => {
- return mount(ProfileSlug, {
- mocks,
- localVue,
- stubs,
- })
- }
+ const Wrapper = (badgesEnabled, data) => {
+ return render(ProfileSlug, {
+ localVue,
+ stubs,
+ data: () => data,
+ mocks: {
+ ...mocks,
+ $env: {
+ BADGES_ENABLED: badgesEnabled,
+ },
+ },
+ })
+ }
- describe('given an authenticated user', () => {
- beforeEach(() => {
- mocks.$filters = {
- removeLinks: (c) => c,
- truncate: (a) => a,
- }
- mocks.$store = {
- getters: {
- 'auth/isModerator': () => false,
- 'auth/user': {
- id: 'u23',
- },
+ describe('given an authenticated user', () => {
+ beforeEach(() => {
+ mocks.$filters = {
+ removeLinks: (c) => c,
+ truncate: (a) => a,
+ }
+ mocks.$store = {
+ getters: {
+ 'auth/isModerator': () => false,
+ 'auth/user': {
+ id: 'u23',
},
- }
- })
+ },
+ }
+ })
- describe('given a user for the profile', () => {
- beforeEach(() => {
- wrapper = Wrapper()
- wrapper.setData({
- User: [
+ describe('given another profile user', () => {
+ const user = {
+ User: [
+ {
+ id: 'u3',
+ name: 'Bob the builder',
+ contributionsCount: 6,
+ shoutedCount: 7,
+ commentedCount: 8,
+ badgeVerification: {
+ id: 'bv1',
+ icon: '/path/to/icon-bv1',
+ description: 'verified',
+ isDefault: false,
+ },
+ badgeTrophiesSelected: [
{
- id: 'u3',
- name: 'Bob the builder',
- contributionsCount: 6,
- shoutedCount: 7,
- commentedCount: 8,
+ id: 'bt1',
+ icon: '/path/to/icon-bt1',
+ description: 'a trophy',
+ isDefault: false,
+ },
+ {
+ id: 'bt2',
+ icon: '/path/to/icon-bt2',
+ description: 'no trophy',
+ isDefault: true,
},
],
- })
+ },
+ ],
+ }
+
+ describe('and badges are enabled', () => {
+ beforeEach(() => {
+ wrapper = Wrapper(true, user)
})
- it('displays name of the user', () => {
- expect(wrapper.text()).toContain('Bob the builder')
+ it('renders', () => {
+ expect(wrapper.container).toMatchSnapshot()
+ })
+ })
+
+ describe('and badges are disabled', () => {
+ beforeEach(() => {
+ wrapper = Wrapper(false, user)
+ })
+
+ it('renders', () => {
+ expect(wrapper.container).toMatchSnapshot()
+ })
+ })
+ })
+
+ describe('given the logged in user as profile user', () => {
+ beforeEach(() => {
+ mocks.$route.params.id = 'u23'
+ })
+
+ const user = {
+ User: [
+ {
+ id: 'u23',
+ name: 'Bob the builder',
+ contributionsCount: 6,
+ shoutedCount: 7,
+ commentedCount: 8,
+ badgeVerification: {
+ id: 'bv1',
+ icon: '/path/to/icon-bv1',
+ description: 'verified',
+ isDefault: false,
+ },
+ badgeTrophiesSelected: [
+ {
+ id: 'bt1',
+ icon: '/path/to/icon-bt1',
+ description: 'a trophy',
+ isDefault: false,
+ },
+ {
+ id: 'bt2',
+ icon: '/path/to/icon-bt2',
+ description: 'no trophy',
+ isDefault: true,
+ },
+ ],
+ },
+ ],
+ }
+
+ describe('and badges are enabled', () => {
+ beforeEach(() => {
+ wrapper = Wrapper(true, user)
+ })
+
+ it('renders', () => {
+ expect(wrapper.container).toMatchSnapshot()
+ })
+ })
+
+ describe('and badges are disabled', () => {
+ beforeEach(() => {
+ wrapper = Wrapper(false, user)
+ })
+
+ it('renders', () => {
+ expect(wrapper.container).toMatchSnapshot()
})
})
})
diff --git a/webapp/pages/profile/_id/_slug.vue b/webapp/pages/profile/_id/_slug.vue
index 382350faf..38035e217 100644
--- a/webapp/pages/profile/_id/_slug.vue
+++ b/webapp/pages/profile/_id/_slug.vue
@@ -42,8 +42,11 @@
{{ $t('profile.memberSince') }} {{ user.createdAt | date('MMMM yyyy') }}
-
-
+
+
+
+
+
@@ -266,6 +269,10 @@ export default {
user() {
return this.User ? this.User[0] : {}
},
+ userBadges() {
+ if (!this.$env.BADGES_ENABLED) return null
+ return [this.user.badgeVerification, ...(this.user.badgeTrophiesSelected || [])]
+ },
userName() {
const { name } = this.user || {}
return name || this.$t('profile.userAnonym')
@@ -456,6 +463,12 @@ export default {
margin: auto;
margin-top: -60px;
}
+.badge-edit-link {
+ transition: all 0.2s ease-out;
+ &:hover {
+ opacity: 0.7;
+ }
+}
.page-name-profile-id-slug {
.ds-flex-item:first-child .content-menu {
position: absolute;
diff --git a/webapp/pages/registration.spec.js b/webapp/pages/registration.spec.js
index b34ba3ba7..8be08fa5e 100644
--- a/webapp/pages/registration.spec.js
+++ b/webapp/pages/registration.spec.js
@@ -313,7 +313,7 @@ describe('Registration', () => {
it('renders', async () => {
wrapper = await Wrapper()
- expect(wrapper.classes('registration-slider')).toBe(true)
+ expect(wrapper.find('.registration-slider')).toBeTruthy()
})
// The asyncTests must go last
diff --git a/webapp/pages/settings.spec.js b/webapp/pages/settings.spec.js
index 0f3c6e22c..8c2917c90 100644
--- a/webapp/pages/settings.spec.js
+++ b/webapp/pages/settings.spec.js
@@ -1,4 +1,4 @@
-import { mount } from '@vue/test-utils'
+import { render } from '@testing-library/vue'
import settings from './settings.vue'
const localVue = global.localVue
@@ -17,21 +17,37 @@ describe('settings.vue', () => {
}
})
- describe('mount', () => {
- const Wrapper = () => {
- return mount(settings, {
- mocks,
- localVue,
- stubs,
- })
- }
+ const Wrapper = () => {
+ return render(settings, {
+ mocks,
+ localVue,
+ stubs,
+ })
+ }
+ describe('given badges are enabled', () => {
beforeEach(() => {
+ mocks.$env = {
+ BADGES_ENABLED: true,
+ }
wrapper = Wrapper()
})
it('renders', () => {
- expect(wrapper.element.tagName).toBe('DIV')
+ expect(wrapper.container).toMatchSnapshot()
+ })
+ })
+
+ describe('given badges are disabled', () => {
+ beforeEach(() => {
+ mocks.$env = {
+ BADGES_ENABLED: false,
+ }
+ wrapper = Wrapper()
+ })
+
+ it('renders', () => {
+ expect(wrapper.container).toMatchSnapshot()
})
})
})
diff --git a/webapp/pages/settings.vue b/webapp/pages/settings.vue
index 5d526c3cc..e1181650e 100644
--- a/webapp/pages/settings.vue
+++ b/webapp/pages/settings.vue
@@ -21,7 +21,7 @@
export default {
computed: {
routes() {
- return [
+ const routes = [
{
name: this.$t('settings.data.name'),
path: `/settings`,
@@ -83,6 +83,15 @@ export default {
},
} */
]
+
+ if (this.$env.BADGES_ENABLED) {
+ routes.splice(2, 0, {
+ name: this.$t('settings.badges.name'),
+ path: `/settings/badges`,
+ })
+ }
+
+ return routes
},
},
}
diff --git a/webapp/pages/settings/__snapshots__/badges.spec.js.snap b/webapp/pages/settings/__snapshots__/badges.spec.js.snap
new file mode 100644
index 000000000..4f80bc37e
--- /dev/null
+++ b/webapp/pages/settings/__snapshots__/badges.spec.js.snap
@@ -0,0 +1,429 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`badge settings with badges more badges available selecting an empty slot shows list with available badges 1`] = `
+
+
+
+ settings.badges.name
+
+
+
+ settings.badges.description
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ settings.badges.click-to-use
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Fourth description
+
+
+
+
+
+
+
+
+
+
+ Fifth description
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`badge settings with badges no more badges available selecting an empty slot shows no more badges available message 1`] = `
+
+
+
+ settings.badges.name
+
+
+
+ settings.badges.description
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ settings.badges.no-badges-available
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`badge settings with badges renders 1`] = `
+
+
+
+ settings.badges.name
+
+
+
+ settings.badges.description
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ settings.badges.click-to-select
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`badge settings with badges selecting a used badge clicking remove badge button with successful server request removes the badge 1`] = `
+
+
+
+ settings.badges.name
+
+
+
+ settings.badges.description
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`badge settings without badges renders 1`] = `
+
+
+
+ settings.badges.name
+
+
+
+ settings.badges.description
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/webapp/pages/settings/__snapshots__/notifications.spec.js.snap b/webapp/pages/settings/__snapshots__/notifications.spec.js.snap
index 0b70393ee..fd2c2f56c 100644
--- a/webapp/pages/settings/__snapshots__/notifications.spec.js.snap
+++ b/webapp/pages/settings/__snapshots__/notifications.spec.js.snap
@@ -12,11 +12,11 @@ exports[`notifications.vue mount renders 1`] = `
settings.notifications.post
@@ -66,11 +66,11 @@ exports[`notifications.vue mount renders 1`] = `
settings.notifications.group
@@ -155,42 +155,47 @@ exports[`notifications.vue mount renders 1`] = `
-
-
+
+
+
+
+
+ settings.notifications.checkAll
+
+
-
+
+
+
+
+
+ settings.notifications.uncheckAll
+
+
- settings.notifications.checkAll
-
-
-
-
-
-
-
-
- settings.notifications.uncheckAll
-
-
-
-
-
-
-
-
- actions.save
-
-
+
+
+
+
+
+ actions.save
+
+
+
diff --git a/webapp/pages/settings/badges.spec.js b/webapp/pages/settings/badges.spec.js
new file mode 100644
index 000000000..291fd75d6
--- /dev/null
+++ b/webapp/pages/settings/badges.spec.js
@@ -0,0 +1,302 @@
+import { render, screen, fireEvent } from '@testing-library/vue'
+import '@testing-library/jest-dom'
+import badges from './badges.vue'
+
+const localVue = global.localVue
+
+describe('badge settings', () => {
+ let mocks
+
+ const apolloMutateMock = jest.fn()
+
+ const Wrapper = () => {
+ return render(badges, {
+ localVue,
+ mocks,
+ })
+ }
+
+ beforeEach(() => {
+ mocks = {
+ $t: jest.fn((t) => t),
+ $toast: {
+ success: jest.fn(),
+ error: jest.fn(),
+ },
+ $apollo: {
+ mutate: apolloMutateMock,
+ },
+ }
+ })
+
+ describe('without badges', () => {
+ beforeEach(() => {
+ mocks.$store = {
+ getters: {
+ 'auth/isModerator': () => false,
+ 'auth/user': {
+ id: 'u23',
+ badgeVerification: {
+ id: 'bv1',
+ icon: '/verification/icon',
+ description: 'Verification description',
+ isDefault: true,
+ },
+ badgeTrophiesSelected: [],
+ badgeTrophiesUnused: [],
+ },
+ },
+ }
+ })
+
+ it('renders', () => {
+ const wrapper = Wrapper()
+ expect(wrapper.container).toMatchSnapshot()
+ })
+ })
+
+ describe('with badges', () => {
+ const badgeTrophiesSelected = [
+ {
+ id: '1',
+ icon: '/path/to/some/icon',
+ isDefault: false,
+ description: 'Some description',
+ },
+ {
+ id: '2',
+ icon: '/path/to/empty/icon',
+ isDefault: true,
+ description: 'Empty',
+ },
+ {
+ id: '3',
+ icon: '/path/to/third/icon',
+ isDefault: false,
+ description: 'Third description',
+ },
+ ]
+
+ const badgeTrophiesUnused = [
+ {
+ id: '4',
+ icon: '/path/to/fourth/icon',
+ description: 'Fourth description',
+ },
+ {
+ id: '5',
+ icon: '/path/to/fifth/icon',
+ description: 'Fifth description',
+ },
+ ]
+
+ let wrapper
+
+ beforeEach(() => {
+ mocks.$store = {
+ getters: {
+ 'auth/isModerator': () => false,
+ 'auth/user': {
+ id: 'u23',
+ badgeVerification: {
+ id: 'bv1',
+ icon: '/verification/icon',
+ description: 'Verification description',
+ isDefault: false,
+ },
+ badgeTrophiesSelected,
+ badgeTrophiesUnused,
+ },
+ },
+ }
+ wrapper = Wrapper()
+ })
+
+ it('renders', () => {
+ expect(wrapper.container).toMatchSnapshot()
+ })
+
+ describe('selecting a used badge', () => {
+ beforeEach(async () => {
+ const badge = screen.getByTitle(badgeTrophiesSelected[0].description)
+ await fireEvent.click(badge)
+ })
+
+ it('shows remove badge button', () => {
+ expect(screen.getByText('settings.badges.remove')).toBeInTheDocument()
+ })
+
+ describe('clicking remove badge button', () => {
+ const clickButton = async () => {
+ const removeButton = screen.getByText('settings.badges.remove')
+ await fireEvent.click(removeButton)
+ }
+
+ describe('with successful server request', () => {
+ beforeEach(() => {
+ apolloMutateMock.mockResolvedValue({
+ data: {
+ setTrophyBadgeSelected: {
+ id: 'u23',
+ badgeTrophiesSelected: [
+ {
+ id: '2',
+ icon: '/path/to/empty/icon',
+ isDefault: true,
+ description: 'Empty',
+ },
+ {
+ id: '2',
+ icon: '/path/to/empty/icon',
+ isDefault: true,
+ description: 'Empty',
+ },
+ {
+ id: '3',
+ icon: '/path/to/third/icon',
+ isDefault: false,
+ description: 'Third description',
+ },
+ ],
+ },
+ },
+ })
+ clickButton()
+ })
+
+ it('calls the server', () => {
+ expect(apolloMutateMock).toHaveBeenCalledWith({
+ mutation: expect.anything(),
+ update: expect.anything(),
+ variables: {
+ badgeId: null,
+ slot: 0,
+ },
+ })
+ })
+
+ /* To test this, we would need a better apollo mock */
+ it.skip('removes the badge', async () => {
+ expect(wrapper.container).toMatchSnapshot()
+ })
+
+ it('shows a success message', () => {
+ expect(mocks.$toast.success).toHaveBeenCalledWith('settings.badges.success-update')
+ })
+ })
+
+ describe('with failed server request', () => {
+ beforeEach(() => {
+ apolloMutateMock.mockRejectedValue({ message: 'Ouch!' })
+ clickButton()
+ })
+
+ it('shows an error message', () => {
+ expect(mocks.$toast.error).toHaveBeenCalledWith('settings.badges.error-update')
+ })
+ })
+ })
+ })
+
+ describe('no more badges available', () => {
+ beforeEach(async () => {
+ mocks.$store.getters['auth/user'].badgeTrophiesUnused = []
+ })
+
+ describe('selecting an empty slot', () => {
+ beforeEach(async () => {
+ const emptySlot = screen.getAllByTitle('Empty')[0]
+ await fireEvent.click(emptySlot)
+ })
+
+ it('shows no more badges available message', () => {
+ expect(wrapper.container).toMatchSnapshot()
+ })
+ })
+ })
+
+ describe('more badges available', () => {
+ describe('selecting an empty slot', () => {
+ beforeEach(async () => {
+ const emptySlot = screen.getAllByTitle('Empty')[0]
+ await fireEvent.click(emptySlot)
+ })
+
+ it('shows list with available badges', () => {
+ expect(wrapper.container).toMatchSnapshot()
+ })
+
+ describe('clicking on an available badge', () => {
+ const clickBadge = async () => {
+ const badge = screen.getByText(badgeTrophiesUnused[0].description)
+ await fireEvent.click(badge)
+ }
+
+ describe('with successful server request', () => {
+ beforeEach(() => {
+ apolloMutateMock.mockResolvedValue({
+ data: {
+ setTrophyBadgeSelected: {
+ id: 'u23',
+ badgeTrophiesSelected: [
+ {
+ id: '4',
+ icon: '/path/to/fourth/icon',
+ description: 'Fourth description',
+ isDefault: false,
+ },
+ {
+ id: '2',
+ icon: '/path/to/empty/icon',
+ isDefault: true,
+ description: 'Empty',
+ },
+ {
+ id: '3',
+ icon: '/path/to/third/icon',
+ isDefault: false,
+ description: 'Third description',
+ },
+ ],
+ },
+ },
+ })
+ clickBadge()
+ })
+
+ it('calls the server', () => {
+ expect(apolloMutateMock).toHaveBeenCalledWith({
+ mutation: expect.anything(),
+ update: expect.anything(),
+ variables: {
+ badgeId: '4',
+ slot: 1,
+ },
+ })
+ })
+
+ /* To test this, we would need a better apollo mock */
+ it.skip('adds the badge', async () => {
+ expect(wrapper.container).toMatchSnapshot()
+ })
+
+ it('shows a success message', () => {
+ expect(mocks.$toast.success).toHaveBeenCalledWith('settings.badges.success-update')
+ })
+ })
+
+ describe('with failed server request', () => {
+ beforeEach(() => {
+ apolloMutateMock.mockRejectedValue({ message: 'Ouch!' })
+ clickBadge()
+ })
+
+ it('shows an error message', () => {
+ expect(mocks.$toast.error).toHaveBeenCalledWith('settings.badges.error-update')
+ })
+ })
+ })
+ })
+ })
+ })
+})
diff --git a/webapp/pages/settings/badges.vue b/webapp/pages/settings/badges.vue
new file mode 100644
index 000000000..3f0e7c7e7
--- /dev/null
+++ b/webapp/pages/settings/badges.vue
@@ -0,0 +1,176 @@
+
+
+ {{ $t('settings.badges.name') }}
+ {{ $t('settings.badges.description') }}
+
+
+
+
+
+
+ {{ $t('settings.badges.no-badges-available') }}
+
+
+
+
+ {{
+ selectedBadgeIndex === null
+ ? this.$t('settings.badges.click-to-select')
+ : isEmptySlotSelected
+ ? this.$t('settings.badges.click-to-use')
+ : ''
+ }}
+
+
+
+
+
+ {{ $t('settings.badges.remove') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/webapp/static/img/badges/stars.svg b/webapp/static/img/badges/stars.svg
new file mode 100644
index 000000000..44d64a5f4
--- /dev/null
+++ b/webapp/static/img/badges/stars.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/webapp/yarn.lock b/webapp/yarn.lock
index 1ef19363e..e17834008 100644
--- a/webapp/yarn.lock
+++ b/webapp/yarn.lock
@@ -7,6 +7,11 @@
resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf"
integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==
+"@adobe/css-tools@^4.4.0":
+ version "4.4.2"
+ resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.4.2.tgz#c836b1bd81e6d62cd6cdf3ee4948bcdce8ea79c8"
+ integrity sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A==
+
"@ampproject/remapping@^2.2.0":
version "2.2.1"
resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630"
@@ -4253,6 +4258,19 @@
lz-string "^1.5.0"
pretty-format "^27.0.2"
+"@testing-library/jest-dom@^6.6.3":
+ version "6.6.3"
+ resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz#26ba906cf928c0f8172e182c6fe214eb4f9f2bd2"
+ integrity sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==
+ dependencies:
+ "@adobe/css-tools" "^4.4.0"
+ aria-query "^5.0.0"
+ chalk "^3.0.0"
+ css.escape "^1.5.1"
+ dom-accessibility-api "^0.6.3"
+ lodash "^4.17.21"
+ redent "^3.0.0"
+
"@testing-library/vue@5":
version "5.9.0"
resolved "https://registry.yarnpkg.com/@testing-library/vue/-/vue-5.9.0.tgz#d33c52ae89e076808abe622f70dcbccb1b5d080c"
@@ -6058,6 +6076,11 @@ aria-query@5.1.3:
dependencies:
deep-equal "^2.0.5"
+aria-query@^5.0.0:
+ version "5.3.2"
+ resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.2.tgz#93f81a43480e33a338f19163a3d10a50c01dcd59"
+ integrity sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==
+
arr-diff@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
@@ -8230,6 +8253,11 @@ css-what@2.1, css-what@^2.1.2:
resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2"
integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==
+css.escape@^1.5.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb"
+ integrity sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==
+
csscolorparser@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/csscolorparser/-/csscolorparser-1.0.3.tgz#b34f391eea4da8f3e98231e2ccd8df9c041f171b"
@@ -8793,6 +8821,11 @@ dom-accessibility-api@^0.5.9:
resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453"
integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==
+dom-accessibility-api@^0.6.3:
+ version "0.6.3"
+ resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz#993e925cc1d73f2c662e7d75dd5a5445259a8fd8"
+ integrity sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==
+
dom-converter@^0.2:
version "0.2.0"
resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768"
diff --git a/yarn.lock b/yarn.lock
index a5a09d086..1e3983be3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4074,10 +4074,10 @@ cypress-network-idle@^1.15.0:
resolved "https://registry.yarnpkg.com/cypress-network-idle/-/cypress-network-idle-1.15.0.tgz#e249f08695a46f1ddce18a95d5293937f277cbb3"
integrity sha512-8zU16zhc7S3nMl1NTEEcNsZYlJy/ZzP2zPTTrngGxyXH32Ipake/xfHLZsgrzeWCieiS2AVhQsakhWqFzO3hpw==
-cypress@^14.3.1:
- version "14.3.1"
- resolved "https://registry.yarnpkg.com/cypress/-/cypress-14.3.1.tgz#b0570c0e5b198d930a2c0f640d099e777bec2d2f"
- integrity sha512-/2q06qvHMK3PNiadnRW1Je0lJ43gAFPQJUAK2zIxjr22kugtWxVQznTBLVu1AvRH+RP3oWZhCdWqiEi+0NuqCg==
+cypress@^14.3.2:
+ version "14.3.2"
+ resolved "https://registry.yarnpkg.com/cypress/-/cypress-14.3.2.tgz#04a6ea66c1715119ef41dda5851d75801cc1e226"
+ integrity sha512-n+yGD2ZFFKgy7I3YtVpZ7BcFYrrDMcKj713eOZdtxPttpBjCyw/R8dLlFSsJPouneGN7A/HOSRyPJ5+3/gKDoA==
dependencies:
"@cypress/request" "^3.0.8"
"@cypress/xvfb" "^1.2.4"