JS tests for FilePreview component

Signed-off-by: Vincent Petry <vincent@nextcloud.com>
This commit is contained in:
Vincent Petry 2021-05-21 09:55:04 +02:00
Родитель 8d61c84e2b
Коммит babb4fa043
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: E055D6A4D513575C
3 изменённых файлов: 571 добавлений и 11 удалений

Просмотреть файл

@ -0,0 +1,519 @@
import Vuex from 'vuex'
import { createLocalVue, shallowMount } from '@vue/test-utils'
import { cloneDeep } from 'lodash'
import storeConfig from '../../../../../store/storeConfig'
import { imagePath, generateRemoteUrl } from '@nextcloud/router'
import { loadState } from '@nextcloud/initial-state'
import PlayCircleOutline from 'vue-material-design-icons/PlayCircleOutline'
import FilePreview from './FilePreview'
jest.mock('@nextcloud/initial-state', () => ({
loadState: jest.fn(),
}))
describe('FilePreview.vue', () => {
let store
let localVue
let testStoreConfig
let propsData
let imageMock
let getUserIdMock
let oldPixelRatio
beforeEach(() => {
localVue = createLocalVue()
localVue.use(Vuex)
oldPixelRatio = window.devicePixelRatio
testStoreConfig = cloneDeep(storeConfig)
getUserIdMock = jest.fn().mockReturnValue('current-user-id')
testStoreConfig.modules.actorStore.getters.getUserId = () => getUserIdMock
store = new Vuex.Store(testStoreConfig)
imageMock = {
onload: jest.fn(),
onerror: jest.fn(),
src: null,
}
jest.spyOn(global, 'Image')
.mockImplementation(() => {
return imageMock
})
propsData = {
id: '123',
name: 'test.jpg',
path: 'path/to/test.jpg',
size: 128,
mimetype: 'image/jpeg',
previewAvailable: 'yes',
}
})
afterEach(() => {
jest.clearAllMocks()
window.devicePixelRatio = oldPixelRatio
})
function parseRelativeUrl(url) {
return new URL('https://localhost' + url)
}
describe('file preview rendering', () => {
test('renders file preview', async() => {
const wrapper = shallowMount(FilePreview, {
localVue,
store,
propsData: propsData,
})
await imageMock.onload()
expect(wrapper.element.tagName).toBe('A')
const imageUrl = parseRelativeUrl(wrapper.find('img').attributes('src'))
expect(imageUrl.pathname).toBe('/nc-webroot/core/preview')
expect(imageUrl.searchParams.get('fileId')).toBe('123')
expect(imageUrl.searchParams.get('x')).toBe('-1')
expect(imageUrl.searchParams.get('y')).toBe('384')
expect(imageUrl.searchParams.get('a')).toBe('1')
expect(wrapper.find('.loading').exists()).toBe(false)
})
test('renders file preview for guests', async() => {
propsData.link = 'https://localhost/nc-webroot/s/xtokenx'
getUserIdMock.mockClear().mockReturnValue(null)
const wrapper = shallowMount(FilePreview, {
localVue,
store,
propsData: propsData,
})
await imageMock.onload()
expect(wrapper.element.tagName).toBe('A')
const imageUrl = parseRelativeUrl(wrapper.find('img').attributes('src'))
expect(imageUrl.pathname).toBe('/nc-webroot/apps/files_sharing/publicpreview/xtokenx')
expect(imageUrl.searchParams.has('fileId')).toBe(false)
expect(imageUrl.searchParams.get('x')).toBe('-1')
expect(imageUrl.searchParams.get('y')).toBe('384')
expect(imageUrl.searchParams.get('a')).toBe('1')
expect(wrapper.find('.loading').exists()).toBe(false)
})
test('calculates preview size based on window pixel ratio', async() => {
window.devicePixelRatio = 1.5
const wrapper = shallowMount(FilePreview, {
localVue,
store,
propsData: propsData,
})
await imageMock.onload()
expect(wrapper.element.tagName).toBe('A')
const imageUrl = parseRelativeUrl(wrapper.find('img').attributes('src'))
expect(imageUrl.searchParams.get('y')).toBe('576')
})
test('renders small previews when requested', async() => {
propsData.smallPreview = true
const wrapper = shallowMount(FilePreview, {
localVue,
store,
propsData: propsData,
})
await imageMock.onload()
expect(wrapper.element.tagName).toBe('A')
const imageUrl = parseRelativeUrl(wrapper.find('img').attributes('src'))
expect(imageUrl.searchParams.get('y')).toBe('32')
})
describe('uploading', () => {
let uploadProgressMock
beforeEach(() => {
uploadProgressMock = jest.fn()
testStoreConfig.modules.fileUploadStore.getters.uploadProgress = () => uploadProgressMock
store = new Vuex.Store(testStoreConfig)
})
test('renders progress bar while uploading', async() => {
propsData.id = 'temp-123'
propsData.index = 'index-1'
propsData.uploadId = 1000
propsData.localUrl = 'blob:XYZ'
uploadProgressMock.mockReturnValue(85)
const wrapper = shallowMount(FilePreview, {
localVue,
store,
propsData: propsData,
})
await imageMock.onload()
expect(wrapper.element.tagName).toBe('DIV')
expect(wrapper.find('img').attributes('src')).toBe('blob:XYZ')
const progressEl = wrapper.findComponent({ name: 'ProgressBar' })
expect(progressEl.exists()).toBe(true)
expect(progressEl.props('value')).toBe(85)
expect(uploadProgressMock).toHaveBeenCalledWith(1000, 'index-1')
})
})
test('renders spinner while loading', () => {
const wrapper = shallowMount(FilePreview, {
localVue,
store,
propsData: propsData,
})
expect(wrapper.element.tagName).toBe('A')
expect(wrapper.find('img').exists()).toBe(false)
expect(wrapper.find('.loading').exists()).toBe(true)
})
test('renders default mime icon on load error', async() => {
const wrapper = shallowMount(FilePreview, {
localVue,
store,
propsData: propsData,
})
await imageMock.onerror()
expect(wrapper.element.tagName).toBe('A')
const imageUrl = wrapper.find('img').attributes('src')
expect(imageUrl).toBe(imagePath('core', 'filetypes/file'))
})
test('renders generic mime type icon for unknown mime types', async() => {
propsData.previewAvailable = 'no'
OC.MimeType.getIconUrl.mockReturnValueOnce(imagePath('core', 'image/jpeg'))
const wrapper = shallowMount(FilePreview, {
localVue,
store,
propsData: propsData,
})
await imageMock.onload()
expect(wrapper.element.tagName).toBe('A')
const imageUrl = wrapper.find('img').attributes('src')
expect(imageUrl).toBe(imagePath('core', 'image/jpeg'))
expect(OC.MimeType.getIconUrl).toHaveBeenCalledWith('image/jpeg')
})
describe('gif rendering', () => {
beforeEach(() => {
propsData.mimetype = 'image/gif'
propsData.name = 'test.gif'
propsData.path = 'path/to/test.gif'
loadState.mockImplementation((app, key) => {
if (app === 'core' && key === 'capabilities') {
return {
spreed: {
config: {
previews: {
'max-gif-size': 1024,
},
},
},
}
}
return null
})
})
test('directly renders small GIF files', async() => {
propsData.size = 128
const wrapper = shallowMount(FilePreview, {
localVue,
store,
propsData: propsData,
})
await imageMock.onload()
expect(wrapper.element.tagName).toBe('A')
expect(wrapper.find('img').attributes('src'))
.toBe(generateRemoteUrl('dav/files/current-user-id/path/to/test.gif'))
})
test('directly renders small GIF files (absolute path)', async() => {
propsData.size = 128
propsData.path = '/path/to/test.gif'
const wrapper = shallowMount(FilePreview, {
localVue,
store,
propsData: propsData,
})
await imageMock.onload()
expect(wrapper.element.tagName).toBe('A')
expect(wrapper.find('img').attributes('src'))
.toBe(generateRemoteUrl('dav/files/current-user-id/path/to/test.gif'))
})
test('directly renders small GIF files for guests', async() => {
propsData.size = 128
propsData.link = 'https://localhost/nc-webroot/s/xtokenx'
getUserIdMock.mockClear().mockReturnValue(null)
const wrapper = shallowMount(FilePreview, {
localVue,
store,
propsData: propsData,
})
await imageMock.onload()
expect(wrapper.element.tagName).toBe('A')
expect(wrapper.find('img').attributes('src'))
.toBe(propsData.link + '/download/test.gif')
})
test('renders static preview for big GIF files', async() => {
// bigger than max from capability
propsData.size = 2048
const wrapper = shallowMount(FilePreview, {
localVue,
store,
propsData: propsData,
})
await imageMock.onload()
expect(wrapper.element.tagName).toBe('A')
const imageUrl = parseRelativeUrl(wrapper.find('img').attributes('src'))
expect(imageUrl.pathname).toBe('/nc-webroot/core/preview')
expect(imageUrl.searchParams.get('fileId')).toBe('123')
expect(imageUrl.searchParams.get('x')).toBe('-1')
expect(imageUrl.searchParams.get('y')).toBe('384')
})
})
describe('triggering viewer', () => {
let oldViewer
let oldFiles
let getSidebarStatusMock
beforeEach(() => {
oldViewer = OCA.Viewer
oldFiles = OCA.Files
OCA.Files = {
Sidebar: {
state: {
},
},
}
getSidebarStatusMock = jest.fn().mockReturnValue(true)
testStoreConfig.modules.sidebarStore.getters.getSidebarStatus = getSidebarStatusMock
store = new Vuex.Store(testStoreConfig)
})
afterEach(() => {
if (oldViewer) {
OCA.Viewer = oldViewer
} else {
delete OCA.Viewer
}
if (oldFiles) {
OCA.Files = oldFiles
} else {
delete OCA.Files
}
})
test('opens viewer when clicking if viewer available', async() => {
OCA.Viewer = {
open: jest.fn(),
availableHandlers: [{
mimes: ['image/png', 'image/jpeg'],
}],
}
const wrapper = shallowMount(FilePreview, {
localVue,
store,
propsData: propsData,
})
await imageMock.onload()
await wrapper.find('a').trigger('click')
expect(OCA.Viewer.open).toHaveBeenCalledWith({
list: [{
basename: 'test.jpg',
fileid: 123,
filename: '/path/to/test.jpg',
hasPreview: true,
mime: 'image/jpeg',
}],
path: '/path/to/test.jpg',
})
expect(OCA.Files.Sidebar.state.file).toBe('/path/to/test.jpg')
})
test('does not open viewer when clicking if no mime handler available', async() => {
OCA.Viewer = {
open: jest.fn(),
availableHandlers: [{
mimes: ['image/png'],
}],
}
const wrapper = shallowMount(FilePreview, {
localVue,
store,
propsData: propsData,
})
await imageMock.onload()
await wrapper.find('a').trigger('click')
expect(OCA.Viewer.open).not.toHaveBeenCalled()
})
test('does not open viewer when clicking if viewer is not available', async() => {
delete OCA.Viewer
const wrapper = shallowMount(FilePreview, {
localVue,
store,
propsData: propsData,
})
await imageMock.onload()
// no error
await wrapper.find('a').trigger('click')
})
describe('play icon for video', () => {
beforeEach(() => {
propsData.mimetype = 'video/mp4'
propsData.name = 'test.mp4'
propsData.path = 'path/to/test.mp4'
// viewer needs to be available
OCA.Viewer = {
open: jest.fn(),
availableHandlers: [{
mimes: ['video/mp4', 'image/jpeg', 'image/png', 'image/gif'],
}],
}
})
async function testPlayButtonVisible(visible) {
const wrapper = shallowMount(FilePreview, {
localVue,
store,
propsData: propsData,
})
await imageMock.onload()
const buttonEl = wrapper.findComponent(PlayCircleOutline)
expect(buttonEl.exists()).toBe(visible)
}
test('renders play icon for video previews', async() => {
await testPlayButtonVisible(true)
})
test('does not render play icon for direct renders', async() => {
// gif is directly rendered
propsData.mimetype = 'image/gif'
propsData.name = 'test.gif'
propsData.path = 'path/to/test.gif'
await testPlayButtonVisible(false)
})
test('render play icon gif previews with big size', async() => {
// gif is directly rendered
propsData.mimetype = 'image/gif'
propsData.name = 'test.gif'
propsData.path = 'path/to/test.gif'
propsData.size = 10000000 // bigger than default max
await testPlayButtonVisible(true)
})
test('does not render play icon for small previews', async() => {
propsData.smallPreview = true
await testPlayButtonVisible(false)
})
test('does not render play icon for failed videos', async() => {
const wrapper = shallowMount(FilePreview, {
localVue,
store,
propsData: propsData,
})
await imageMock.onerror()
const buttonEl = wrapper.findComponent(PlayCircleOutline)
expect(buttonEl.exists()).toBe(false)
})
test('does not render play icon if viewer not available', async() => {
delete OCA.Viewer
await testPlayButtonVisible(false)
})
test('does not render play icon for non-videos', async() => {
// viewer supported, but not a video
propsData.mimetype = 'image/png'
propsData.name = 'test.png'
propsData.path = 'path/to/test.png'
await testPlayButtonVisible(false)
})
})
})
})
describe('in upload editor', () => {
beforeEach(() => {
propsData.isUploadEditor = true
})
test('emits event when clicking remove button when inside upload editor', async() => {
const wrapper = shallowMount(FilePreview, {
localVue,
store,
propsData: propsData,
})
await imageMock.onload()
expect(wrapper.element.tagName).toBe('DIV')
await wrapper.find('button').trigger('click')
expect(wrapper.emitted()['remove-file']).toStrictEqual([['123']])
})
})
})

Просмотреть файл

@ -99,34 +99,54 @@ export default {
},
props: {
type: {
type: String,
required: true,
},
/**
* File id
*/
id: {
type: String,
required: true,
},
/**
* File name
*/
name: {
type: String,
required: true,
},
/**
* File path relative to the user's home storage,
* or link share root, includes the file name.
*/
path: {
type: String,
default: '',
},
/**
* File size in bytes
*/
size: {
type: Number,
default: -1,
},
/**
* Download link
*/
link: {
type: String,
default: '',
},
/**
* Mime type
*/
mimetype: {
type: String,
default: '',
},
/**
* Whether a preview is available, string "yes" for yes
* otherwise the string "no"
*/
// FIXME: use booleans here
previewAvailable: {
type: String,
default: 'no',
@ -138,25 +158,38 @@ export default {
type: Boolean,
default: false,
},
// In case this component is used to display a file that is being uploaded
// this parameter is used to access the file upload status in the store
/**
* Upload id from the file upload store.
*
* In case this component is used to display a file that is being uploaded
* this parameter is used to access the file upload status in the store
*/
uploadId: {
type: Number,
default: null,
},
// In case this component is used to display a file that is being uploaded
// this parameter is used to access the file upload status in the store
/**
* File upload index from the file upload store.
*
* In case this component is used to display a file that is being uploaded
* this parameter is used to access the file upload status in the store
*/
index: {
type: String,
default: '',
},
// True if this component is used in the upload editor
/**
* Whether the container is the upload editor.
* True if this component is used in the upload editor.
*/
// FIXME: file-preview should be encapsulated and not be aware of its surroundings
isUploadEditor: {
type: Boolean,
default: false,
},
// The link to the file for displaying it in the preview
/**
* The link to the file for displaying it in the preview
*/
localUrl: {
type: String,
default: '',
@ -323,6 +356,7 @@ export default {
return this.$store.getters.uploadProgress(this.uploadId, this.index)
}
}
// likely never reached
return 0
},
hasTemporaryImageUrl() {
@ -334,7 +368,7 @@ export default {
},
removeAriaLabel() {
return t('spreed', 'Remove' + this.name)
return t('spreed', 'Remove {fileName}', { fileName: this.name })
},
},
mounted() {

Просмотреть файл

@ -27,6 +27,9 @@ import Vue from 'vue'
global.OC = {
requestToken: '123',
webroot: '/nc-webroot',
coreApps: [
'core',
],
config: {
modRewriteWorking: true,
},
@ -41,6 +44,10 @@ global.OC = {
getLocale() {
return 'en_GB'
},
MimeType: {
getIconUrl: jest.fn(),
},
}
global.OCA = {
Talk: {