Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions packages/web-pkg/src/components/AvatarUpload.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<div class="avatar-upload">
<input
ref="fileInputRef"
class="oc-invisible"
class="oc-invisible avatar-file-input"
type="file"
accept="image/jpeg, image/png"
@change="onFileChange"
Expand All @@ -17,10 +17,15 @@
/>
<div>
<div class="oc-button-group">
<oc-button size="small" @click="triggerFileInput">
<oc-button class="avatar-upload-button" size="small" @click="triggerFileInput">
{{ $gettext('Upload') }}
</oc-button>
<oc-button v-if="userAvatar" size="small" @click="showRemoveModal = true">
<oc-button
v-if="userAvatar"
class="avatar-upload-remove-button"
size="small"
@click="showRemoveModal = true"
>
{{ $gettext('Remove') }}
</oc-button>
</div>
Expand Down
1 change: 1 addition & 0 deletions packages/web-pkg/src/composables/piniaStores/avatars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export const useAvatarsStore = defineStore('avatars', () => {

return {
userAvatar,
avatarMap,
getAvatar,
addAvatar,
removeAvatar,
Expand Down
176 changes: 176 additions & 0 deletions packages/web-pkg/tests/unit/components/AvatarUpload.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import AvatarUpload from '../../../src/components/AvatarUpload.vue'
import {
createMockFile,
defaultComponentMocks,
defaultPlugins,
mount,
nextTicks
} from '@opencloud-eu/web-test-helpers'
import { useMessages } from '../../../src'
import { describe } from 'vitest'

vi.mock('cropperjs', () => {
return {
default: vi.fn().mockImplementation(() => ({
getCroppedCanvas: vi.fn(() => ({
toBlob: vi.fn((cb) => cb(new Blob())),
toDataURL: vi.fn(() => 'data:image/png;base64,mocked')
})),
destroy: vi.fn(),
replace: vi.fn(),
reset: vi.fn(),
crop: vi.fn(),
move: vi.fn(),
rotate: vi.fn(),
scale: vi.fn(),
ready: vi.fn(() => true)
}))
}
})

const selectors = {
removeAvatarButton: '.avatar-upload-remove-button',
avatarFileInput: '.avatar-file-input',
modalConfirm: '.oc-modal-body-actions-confirm '
}

describe('AvatarUpload', () => {
describe('removeButton', () => {
it('should exist when user has avatar', () => {
const { wrapper } = getWrapper()
expect(wrapper.find(selectors.removeAvatarButton).exists()).toBeTruthy()
})
it('should not exist when user has no avatar', () => {
const { wrapper } = getWrapper({ userHasAvatar: false })
expect(wrapper.find(selectors.removeAvatarButton).exists()).toBeFalsy()
})
it('should show message on success', async () => {
const { wrapper } = getWrapper()
await wrapper.vm.$nextTick()
await wrapper.find(selectors.removeAvatarButton).trigger('click')
await wrapper.find(selectors.modalConfirm).trigger('click')
const { showMessage } = useMessages()
expect(showMessage).toHaveBeenCalledWith(
expect.objectContaining({
title: 'Profile picture was removed successfully'
})
)
})
it('should show error message on error', async () => {
const { wrapper, mocks } = getWrapper()
mocks.$clientService.graphAuthenticated.photos.deleteOwnUserPhoto.mockRejectedValueOnce(
new Error('')
)
await wrapper.vm.$nextTick()
await wrapper.find(selectors.removeAvatarButton).trigger('click')
await wrapper.find(selectors.modalConfirm).trigger('click')
const { showErrorMessage } = useMessages()
expect(showErrorMessage).toHaveBeenCalledWith(
expect.objectContaining({
title: 'Failed to remove profile picture'
})
)
})
})
describe('uploadButton', () => {
it('should show error message when file size it too big', () => {
const { wrapper } = getWrapper({})
const file = createMockFile('large-file.png', 20 * 1024 * 1024, 'image/png')
const input = wrapper.find(selectors.avatarFileInput).element as HTMLInputElement
const event = new Event('change')

Object.defineProperty(event, 'target', {
writable: false,
value: { files: [file] }
})

input.dispatchEvent(event)
const { showErrorMessage } = useMessages()
expect(showErrorMessage).toHaveBeenCalledWith(
expect.objectContaining({
title: 'File size exceeds the limit of 10MB'
})
)
})

it('should show message on success', async () => {
const { wrapper } = getWrapper()
const file = createMockFile('file.png', 9 * 1024 * 1024, 'image/png')
const input = wrapper.find(selectors.avatarFileInput).element as HTMLInputElement
const event = new Event('change')

Object.defineProperty(event, 'target', {
writable: false,
value: { files: [file] }
})

input.dispatchEvent(event)
;(wrapper.vm as any).cropperReady = true
await nextTicks(2)
await wrapper.find(selectors.modalConfirm).trigger('click')

const { showMessage } = useMessages()
expect(showMessage).toHaveBeenCalledWith(
expect.objectContaining({
title: 'Profile picture was set successfully'
})
)
})
it('should show error message on error', async () => {
const { wrapper, mocks } = getWrapper()
mocks.$clientService.graphAuthenticated.photos.updateOwnUserPhotoPatch.mockRejectedValueOnce(
new Error('')
)

const file = createMockFile('file.png', 9 * 1024 * 1024, 'image/png')
const input = wrapper.find(selectors.avatarFileInput).element as HTMLInputElement
const event = new Event('change')

Object.defineProperty(event, 'target', {
writable: false,
value: { files: [file] }
})

input.dispatchEvent(event)
;(wrapper.vm as any).cropperReady = true
await nextTicks(2)
await wrapper.find(selectors.modalConfirm).trigger('click')

const { showErrorMessage } = useMessages()
expect(showErrorMessage).toHaveBeenCalledWith(
expect.objectContaining({
title: 'Failed to set profile picture'
})
)
})
})
})

const getWrapper = ({ userHasAvatar = true } = {}) => {
const mocks = {
...defaultComponentMocks({})
}

return {
mocks,
wrapper: mount(AvatarUpload, {
global: {
renderStubDefaultSlot: true,
stubs: {
FocusTrap: true
},
plugins: [
...defaultPlugins({
piniaOptions: {
avatarsStore: {
userAvatar: userHasAvatar ? 'https://localhost:9201/some-object-url' : null
}
}
})
],
mocks,
provide: mocks
}
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ exports[`account page > account information section > displays basic user inform
<td class="oc-table-cell oc-table-cell-align-left oc-table-cell-align-middle oc-table-cell-width-auto oc-td">Profile picture</td>
<td class="oc-table-cell oc-table-cell-align-left oc-table-cell-align-middle oc-table-cell-width-auto oc-td">Max. 10MB, JPG, PNG</td>
<td class="oc-table-cell oc-table-cell-align-left oc-table-cell-align-middle oc-table-cell-width-auto oc-td">
<div class="avatar-upload oc-mb-s"><input class="oc-invisible" type="file" accept="image/jpeg, image/png">
<div class="avatar-upload oc-mb-s"><input class="oc-invisible avatar-file-input" type="file" accept="image/jpeg, image/png">
<div class="oc-flex oc-flex-column oc-flex-middle"><span class="vue-avatar--wrapper oc-avatar oc-mb-m" style="width: 128px; height: 128px; line-height: 128px; background-color: #9C27B0; font-size: 51px; font-family: Helvetica, Arial, sans-serif; color: white;" width="128" aria-hidden="true" focusable="false" data-test-user-name="some-displayname" userid="some-username"><span class="avatar-initials" style="color: white;">SD</span></span>
<div>
<div class="oc-button-group"><button type="button" class="oc-button oc-rounded oc-button-s oc-button-justify-content-center oc-button-gap-m oc-button-secondary oc-button-outline oc-button-secondary-outline">
<div class="oc-button-group"><button type="button" class="oc-button oc-rounded oc-button-s oc-button-justify-content-center oc-button-gap-m oc-button-secondary oc-button-outline oc-button-secondary-outline avatar-upload-button">
<!--v-if-->
<!-- @slot Content of the button --> Upload
</button>
Expand Down
10 changes: 10 additions & 0 deletions packages/web-test-helpers/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,16 @@ export const nextTicks = async (amount: number) => {
}
}

export const createMockFile = (
name: string,
sizeInBytes: number,
type = 'application/octet-stream'
): File => {
const buffer = new Uint8Array(sizeInBytes)
const blob = new Blob([buffer], { type })
return new File([blob], name, { type })
}

type DefinedComponent = new (...args: any[]) => any
export type ComponentProps<T extends DefinedComponent> = InstanceType<T>['$props']
export type PartialComponentProps<T extends DefinedComponent> = Partial<ComponentProps<T>>
8 changes: 8 additions & 0 deletions packages/web-test-helpers/src/mocks/pinia.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ export type PiniaMockOptions = {
areDisabledSpacesShown?: boolean
deleteQueue?: string[]
}
avatarsStore?: {
userAvatar?: string
avatarMap?: Record<string, string>
}
sharesState?: {
collaboratorShares?: CollaboratorShare[]
linkShares?: LinkShare[]
Expand All @@ -91,6 +95,7 @@ export function createMockStore({
messagesState = {},
modalsState = {},
resourcesStore = {},
avatarsStore = {},
userSettingsStore = {},
groupSettingsStore = {},
spaceSettingsStore = {},
Expand Down Expand Up @@ -140,6 +145,9 @@ export function createMockStore({
...themeState
},
resources: { resources: [], ...resourcesStore },
avatars: {
...avatarsStore
},
shares: { collaboratorShares: [], linkShares: [], ...sharesState },
spaces: { spaces: [], ...spacesState },
userSettings: { users: [], selectedUsers: [], ...userSettingsStore },
Expand Down