diff --git a/webapp/components/BadgeSelection.spec.js b/webapp/components/BadgeSelection.spec.js
new file mode 100644
index 000000000..78f00b87a
--- /dev/null
+++ b/webapp/components/BadgeSelection.spec.js
@@ -0,0 +1,76 @@
+import { render, screen, fireEvent } from '@testing-library/vue'
+import BadgeSelection from './BadgeSelection.vue'
+
+const localVue = global.localVue
+
+describe('Badges.vue', () => {
+ const Wrapper = (propsData) => {
+ return render(BadgeSelection, {
+ propsData,
+ localVue,
+ })
+ }
+
+ describe('without badges', () => {
+ it('renders', () => {
+ const wrapper = Wrapper({ badges: [] })
+ expect(wrapper.container).toMatchSnapshot()
+ })
+ })
+
+ describe('with badges', () => {
+ const badges = [
+ {
+ id: '1',
+ icon: '/path/to/some/icon',
+ isDefault: false,
+ description: 'Some description',
+ },
+ {
+ id: '2',
+ icon: '/path/to/another/icon',
+ isDefault: true,
+ description: 'Another description',
+ },
+ {
+ id: '3',
+ icon: '/path/to/third/icon',
+ isDefault: false,
+ description: 'Third description',
+ },
+ ]
+
+ let wrapper
+
+ beforeEach(() => {
+ wrapper = Wrapper({ badges })
+ })
+
+ it('renders', () => {
+ expect(wrapper.container).toMatchSnapshot()
+ })
+
+ describe('clicking on a badge', () => {
+ beforeEach(async () => {
+ const badge = screen.getByText(badges[1].description)
+ await fireEvent.click(badge)
+ })
+
+ it('emits badge-selected with badge', async () => {
+ expect(wrapper.emitted()['badge-selected']).toEqual([[badges[1]]])
+ })
+ })
+
+ describe('clicking twice on a badge', () => {
+ beforeEach(async () => {
+ const badge = screen.getByText(badges[1].description)
+ await fireEvent.click(badge)
+ await fireEvent.click(badge)
+ })
+
+ it('emits badge-selected with null', async () => {
+ expect(wrapper.emitted()['badge-selected']).toEqual([[badges[1]], [null]])
+ })
+ })
+ })
+})
diff --git a/webapp/components/BadgeSelection.vue b/webapp/components/BadgeSelection.vue
new file mode 100644
index 000000000..a6554d779
--- /dev/null
+++ b/webapp/components/BadgeSelection.vue
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
+
{{ badge.description }}
+
+
+
+
+
+
+
+
diff --git a/webapp/components/Badges.spec.js b/webapp/components/Badges.spec.js
index d19c2beb2..ae15e0b0a 100644
--- a/webapp/components/Badges.spec.js
+++ b/webapp/components/Badges.spec.js
@@ -1,29 +1,114 @@
-import { shallowMount } from '@vue/test-utils'
+import { render, screen, fireEvent } from '@testing-library/vue'
import Badges from './Badges.vue'
+const localVue = global.localVue
+
describe('Badges.vue', () => {
- let propsData
+ const Wrapper = (propsData) => {
+ return render(Badges, {
+ propsData,
+ localVue,
+ })
+ }
- beforeEach(() => {
- propsData = {}
- })
-
- describe('shallowMount', () => {
- const Wrapper = () => {
- return shallowMount(Badges, { propsData })
- }
-
- it('has class "hc-badges"', () => {
- expect(Wrapper().find('.hc-badges').exists()).toBe(true)
+ describe('without badges', () => {
+ it('renders in presentation mode', () => {
+ const wrapper = Wrapper({ badges: [], selectionMode: false })
+ expect(wrapper.container).toMatchSnapshot()
})
- describe('given a badge', () => {
+ it('renders in selection mode', () => {
+ const wrapper = Wrapper({ badges: [], selectionMode: true })
+ expect(wrapper.container).toMatchSnapshot()
+ })
+ })
+
+ describe('with badges', () => {
+ const badges = [
+ {
+ id: '1',
+ icon: '/path/to/some/icon',
+ isDefault: false,
+ description: 'Some description',
+ },
+ {
+ id: '2',
+ icon: '/path/to/another/icon',
+ isDefault: true,
+ description: 'Another description',
+ },
+ {
+ id: '3',
+ icon: '/path/to/third/icon',
+ isDefault: false,
+ description: 'Third description',
+ },
+ ]
+
+ describe('in presentation mode', () => {
+ let wrapper
+
beforeEach(() => {
- propsData.badges = [{ id: '1', icon: '/path/to/some/icon' }]
+ wrapper = Wrapper({ badges, scale: 1.2, selectionMode: false })
})
- it('proxies badge icon, which is just a URL without metadata', () => {
- expect(Wrapper().find('img[src="/api/path/to/some/icon"]').exists()).toBe(true)
+ it('renders', () => {
+ expect(wrapper.container).toMatchSnapshot()
+ })
+
+ it('clicking on second badge does nothing', async () => {
+ const badge = screen.getByTitle(badges[1].description)
+ await fireEvent.click(badge)
+ expect(wrapper.emitted()).toEqual({})
+ })
+ })
+
+ describe('in selection mode', () => {
+ let wrapper
+
+ beforeEach(() => {
+ wrapper = Wrapper({ badges, scale: 1.2, selectionMode: true })
+ })
+
+ it('renders', () => {
+ expect(wrapper.container).toMatchSnapshot()
+ })
+
+ it('clicking on first badge does nothing', async () => {
+ const badge = screen.getByTitle(badges[0].description)
+ await fireEvent.click(badge)
+ expect(wrapper.emitted()).toEqual({})
+ })
+
+ describe('clicking on second badge', () => {
+ beforeEach(async () => {
+ const badge = screen.getByTitle(badges[1].description)
+ await fireEvent.click(badge)
+ })
+
+ it('selects badge', () => {
+ expect(wrapper.container).toMatchSnapshot()
+ })
+
+ it('emits badge-selected with index', async () => {
+ expect(wrapper.emitted()['badge-selected']).toEqual([[1]])
+ })
+ })
+
+ describe('clicking twice on second badge', () => {
+ beforeEach(async () => {
+ const badge = screen.getByTitle(badges[1].description)
+ await fireEvent.click(badge)
+ await fireEvent.click(badge)
+ })
+
+ it('deselects badge', () => {
+ expect(wrapper.container).toMatchSnapshot()
+ })
+
+ it('emits badge-selected with null', async () => {
+ expect(wrapper.emitted()['badge-selected']).toEqual([[1], [null]])
+ })
})
})
})
diff --git a/webapp/components/Badges.vue b/webapp/components/Badges.vue
index d569452c7..ca5c4f0ef 100644
--- a/webapp/components/Badges.vue
+++ b/webapp/components/Badges.vue
@@ -1,69 +1,171 @@
-
-
-
+
+
+
diff --git a/webapp/components/__snapshots__/BadgeSelection.spec.js.snap b/webapp/components/__snapshots__/BadgeSelection.spec.js.snap
new file mode 100644
index 000000000..a31d547c9
--- /dev/null
+++ b/webapp/components/__snapshots__/BadgeSelection.spec.js.snap
@@ -0,0 +1,84 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Badges.vue with badges renders 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Another description
+
+
+
+
+
+
+
+
+
+
+ Third description
+
+
+
+
+
+`;
+
+exports[`Badges.vue without badges renders 1`] = `
+
+`;
diff --git a/webapp/components/__snapshots__/Badges.spec.js.snap b/webapp/components/__snapshots__/Badges.spec.js.snap
new file mode 100644
index 000000000..6ea612a76
--- /dev/null
+++ b/webapp/components/__snapshots__/Badges.spec.js.snap
@@ -0,0 +1,165 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Badges.vue with badges in presentation mode renders 1`] = `
+
+`;
+
+exports[`Badges.vue with badges in selection mode clicking on second badge selects badge 1`] = `
+
+`;
+
+exports[`Badges.vue with badges in selection mode clicking twice on second badge deselects badge 1`] = `
+
+`;
+
+exports[`Badges.vue with badges in selection mode renders 1`] = `
+
+`;
+
+exports[`Badges.vue without badges renders in presentation mode 1`] = `
+
+`;
+
+exports[`Badges.vue without badges renders in selection mode 1`] = `
+
+`;
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 ce122672d..4fe39ce24 100644
--- a/webapp/locales/de.json
+++ b/webapp/locales/de.json
@@ -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 f178da549..bdd9cdefb 100644
--- a/webapp/locales/en.json
+++ b/webapp/locales/en.json
@@ -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..60e65ca20 100644
--- a/webapp/locales/es.json
+++ b/webapp/locales/es.json
@@ -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..e7b4fcd4a 100644
--- a/webapp/locales/fr.json
+++ b/webapp/locales/fr.json
@@ -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..703b27fff 100644
--- a/webapp/locales/it.json
+++ b/webapp/locales/it.json
@@ -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..f6a53d0c8 100644
--- a/webapp/locales/nl.json
+++ b/webapp/locales/nl.json
@@ -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..4e928f417 100644
--- a/webapp/locales/pl.json
+++ b/webapp/locales/pl.json
@@ -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..005eb77f4 100644
--- a/webapp/locales/pt.json
+++ b/webapp/locales/pt.json
@@ -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..bc862bcc2 100644
--- a/webapp/locales/ru.json
+++ b/webapp/locales/ru.json
@@ -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/package.json b/webapp/package.json
index d18a88408..d11ffd4ab 100644
--- a/webapp/package.json
+++ b/webapp/package.json
@@ -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..b72a2617f
--- /dev/null
+++ b/webapp/pages/admin/users/__snapshots__/index.spec.js.snap
@@ -0,0 +1,847 @@
+// 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ admin.users.table.edit
+
+
+
+
+
+
+ NaN.
+
+
+
+
+ User
+
+
+
+
+
+
+ user2@example.org
+
+
+
+
+
+
+ user
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ admin.users.table.edit
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
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..fd08f1e0c 100644
--- a/webapp/pages/admin/users/index.vue
+++ b/webapp/pages/admin/users/index.vue
@@ -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: {
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/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..358327202
--- /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/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"