refactor: move OC.MimeType to src and add vitest unit tests

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
This commit is contained in:
Ferdinand Thiessen 2025-10-02 23:12:15 +02:00
parent 057c0dcc98
commit 2e11b96537
No known key found for this signature in database
GPG key ID: 45FAE7268762B400
6 changed files with 214 additions and 242 deletions

View file

@ -1,5 +1,4 @@
[
"mimetype.js",
"mimetypelist.js",
"select2-toggleselect.js"
]

View file

@ -1,106 +0,0 @@
/**
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2015 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-only
*/
/**
* Namespace to hold functions related to convert mimetype to icons
*
* @namespace
*/
OC.MimeType = {
/**
* Cache that maps mimeTypes to icon urls
*/
_mimeTypeIcons: {},
/**
* Return the file icon we want to use for the given mimeType.
* The file needs to be present in the supplied file list
*
* @param {string} mimeType The mimeType we want an icon for
* @param {Array} files The available icons in this theme
* @return {string} The icon to use or null if there is no match
*/
_getFile: function(mimeType, files) {
const icon = mimeType.replace(new RegExp('/', 'g'), '-')
// Generate path
if (mimeType === 'dir' && files.includes('folder')) {
return 'folder'
} else if (mimeType === 'dir-encrypted' && files.includes('folder-encrypted')) {
return 'folder-encrypted'
} else if (mimeType === 'dir-shared' && files.includes('folder-shared')) {
return 'folder-shared'
} else if (mimeType === 'dir-public' && files.includes('folder-public')) {
return 'folder-public'
} else if ((mimeType === 'dir-external' || mimeType === 'dir-external-root') && files.includes('folder-external')) {
return 'folder-external'
} else if (files.includes(icon)) {
return icon
} else if (files.includes(icon.split('-')[0])) {
return icon.split('-')[0]
} else if (files.includes('file')) {
return 'file'
}
return null
},
/**
* Return the url to icon of the given mimeType
*
* @param {string} mimeType The mimeType to get the icon for
* @return {string} Url to the icon for mimeType
*/
getIconUrl: function(mimeType) {
if (typeof mimeType === 'undefined') {
return undefined
}
while (mimeType in OC.MimeTypeList.aliases) {
mimeType = OC.MimeTypeList.aliases[mimeType]
}
if (mimeType in OC.MimeType._mimeTypeIcons) {
return OC.MimeType._mimeTypeIcons[mimeType]
}
// First try to get the correct icon from the current theme
let gotIcon = null
let path = ''
if (OC.theme.folder !== '' && Array.isArray(OC.MimeTypeList.themes[OC.theme.folder])) {
path = OC.getRootPath() + '/themes/' + OC.theme.folder + '/core/img/filetypes/'
const icon = OC.MimeType._getFile(mimeType, OC.MimeTypeList.themes[OC.theme.folder])
if (icon !== null) {
gotIcon = true
path += icon
}
}
if (OCA.Theming && gotIcon === null) {
path = OC.generateUrl('/apps/theming/img/core/filetypes/')
path += OC.MimeType._getFile(mimeType, OC.MimeTypeList.files)
gotIcon = true
}
// If we do not yet have an icon fall back to the default
if (gotIcon === null) {
path = OC.getRootPath() + '/core/img/filetypes/'
path += OC.MimeType._getFile(mimeType, OC.MimeTypeList.files)
}
path += '.svg'
if (OCA.Theming) {
path += '?v=' + OCA.Theming.cacheBuster
}
// Cache the result
OC.MimeType._mimeTypeIcons[mimeType] = path
return path
},
}

View file

@ -1,135 +0,0 @@
/**
* SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2015 ownCloud Inc.
* SPDX-License-Identifier: AGPL-3.0-only
*/
describe('MimeType tests', function() {
let _files
let _aliases
let _theme
beforeEach(function() {
_files = OC.MimeTypeList.files
_aliases = OC.MimeTypeList.aliases
_theme = OC.MimeTypeList.themes.abc
OC.MimeTypeList.files = ['folder', 'folder-shared', 'folder-external', 'foo-bar', 'foo', 'file']
OC.MimeTypeList.aliases = { 'app/foobar': 'foo/bar' }
OC.MimeTypeList.themes.abc = ['folder']
})
afterEach(function() {
OC.MimeTypeList.files = _files
OC.MimeTypeList.aliases = _aliases
OC.MimeTypeList.themes.abc = _theme
})
describe('_getFile', function() {
it('returns the correct icon for "dir"', function() {
const res = OC.MimeType._getFile('dir', OC.MimeTypeList.files)
expect(res).toEqual('folder')
})
it('returns the correct icon for "dir-shared"', function() {
const res = OC.MimeType._getFile('dir-shared', OC.MimeTypeList.files)
expect(res).toEqual('folder-shared')
})
it('returns the correct icon for "dir-external"', function() {
const res = OC.MimeType._getFile('dir-external', OC.MimeTypeList.files)
expect(res).toEqual('folder-external')
})
it('returns the correct icon for a mimetype for which we have an icon', function() {
const res = OC.MimeType._getFile('foo/bar', OC.MimeTypeList.files)
expect(res).toEqual('foo-bar')
})
it('returns the correct icon for a mimetype for which we only have a general mimetype icon', function() {
const res = OC.MimeType._getFile('foo/baz', OC.MimeTypeList.files)
expect(res).toEqual('foo')
})
it('return the file mimetype if we have no matching icon but do have a file icon', function() {
const res = OC.MimeType._getFile('foobar', OC.MimeTypeList.files)
expect(res).toEqual('file')
})
it('return null if we do not have a matching icon', function() {
const res = OC.MimeType._getFile('xyz', [])
expect(res).toEqual(null)
})
})
describe('getIconUrl', function() {
describe('no theme', function() {
let _themeFolder
beforeEach(function() {
_themeFolder = OC.theme.folder
OC.theme.folder = ''
// Clear mimetypeIcons caches
OC.MimeType._mimeTypeIcons = {}
})
afterEach(function() {
OC.theme.folder = _themeFolder
})
it('return undefined if the an icon for undefined is requested', function() {
const res = OC.MimeType.getIconUrl(undefined)
expect(res).toEqual(undefined)
})
it('return the url for the mimetype file', function() {
const res = OC.MimeType.getIconUrl('file')
expect(res).toEqual(OC.getRootPath() + '/core/img/filetypes/file.svg')
})
it('test if the cache works correctly', function() {
OC.MimeType._mimeTypeIcons = {}
expect(Object.keys(OC.MimeType._mimeTypeIcons).length).toEqual(0)
let res = OC.MimeType.getIconUrl('dir')
expect(Object.keys(OC.MimeType._mimeTypeIcons).length).toEqual(1)
expect(OC.MimeType._mimeTypeIcons.dir).toEqual(res)
res = OC.MimeType.getIconUrl('dir-shared')
expect(Object.keys(OC.MimeType._mimeTypeIcons).length).toEqual(2)
expect(OC.MimeType._mimeTypeIcons['dir-shared']).toEqual(res)
})
it('test if alaiases are converted correctly', function() {
const res = OC.MimeType.getIconUrl('app/foobar')
expect(res).toEqual(OC.getRootPath() + '/core/img/filetypes/foo-bar.svg')
expect(OC.MimeType._mimeTypeIcons['foo/bar']).toEqual(res)
})
})
describe('themes', function() {
let _themeFolder
beforeEach(function() {
_themeFolder = OC.theme.folder
OC.theme.folder = 'abc'
// Clear mimetypeIcons caches
OC.MimeType._mimeTypeIcons = {}
})
afterEach(function() {
OC.theme.folder = _themeFolder
})
it('test if theme path is used if a theme icon is availble', function() {
const res = OC.MimeType.getIconUrl('dir')
expect(res).toEqual(OC.getRootPath() + '/themes/abc/core/img/filetypes/folder.svg')
})
it('test if we fallback to the default theme if no icon is available in the theme', function() {
const res = OC.MimeType.getIconUrl('dir-shared')
expect(res).toEqual(OC.getRootPath() + '/core/img/filetypes/folder-shared.svg')
})
})
})
})

View file

@ -63,6 +63,7 @@ import {
showMenu,
unregisterMenu,
} from './menu.js'
import * as MimeType from './mimeType.js'
import msg from './msg.js'
import { redirect, reload } from './navigation.js'
import Notification from './notification.js'
@ -127,6 +128,7 @@ export default {
currentUser,
dialogs: Dialogs,
EventSource,
MimeType,
/**
* Returns the currently logged in user or null if there is no logged in
* user (public page mode)

94
core/src/OC/mimeType.js Normal file
View file

@ -0,0 +1,94 @@
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { generateUrl } from '@nextcloud/router'
const iconCache = new Map()
/**
* Return the url to icon of the given mimeType
*
* @param {string} mimeType The mimeType to get the icon for
* @return {string} Url to the icon for mimeType
*/
export function getIconUrl(mimeType) {
if (typeof mimeType === 'undefined') {
return undefined
}
while (mimeType in window.OC.MimeTypeList.aliases) {
mimeType = window.OC.MimeTypeList.aliases[mimeType]
}
if (!iconCache.has(mimeType)) {
// First try to get the correct icon from the current theme
let gotIcon = null
let path = ''
if (OC.theme.folder !== '' && Array.isArray(OC.MimeTypeList.themes[OC.theme.folder])) {
path = generateUrl('/themes/' + window.OC.theme.folder + '/core/img/filetypes/')
const icon = getMimeTypeIcon(mimeType, window.OC.MimeTypeList.themes[OC.theme.folder])
if (icon !== null) {
gotIcon = true
path += icon
}
}
if (window.OCA.Theming && gotIcon === null) {
path = generateUrl('/apps/theming/img/core/filetypes/')
path += getMimeTypeIcon(mimeType, window.OC.MimeTypeList.files)
gotIcon = true
}
// If we do not yet have an icon fall back to the default
if (gotIcon === null) {
path = generateUrl('/core/img/filetypes/')
path += getMimeTypeIcon(mimeType, window.OC.MimeTypeList.files)
}
path += '.svg'
if (window.OCA.Theming) {
path += '?v=' + window.OCA.Theming.cacheBuster
}
// Cache the result
iconCache.set(mimeType, path)
}
return iconCache.get(mimeType)
}
/**
* Return the file icon we want to use for the given mimeType.
* The file needs to be present in the supplied file list
*
* @param {string} mimeType The mimeType we want an icon for
* @param {string[]} files The available icons in this theme
* @return {string | null} The icon to use or null if there is no match
*/
function getMimeTypeIcon(mimeType, files) {
const icon = mimeType.replace(new RegExp('/', 'g'), '-')
// Generate path
if (mimeType === 'dir' && files.includes('folder')) {
return 'folder'
} else if (mimeType === 'dir-encrypted' && files.includes('folder-encrypted')) {
return 'folder-encrypted'
} else if (mimeType === 'dir-shared' && files.includes('folder-shared')) {
return 'folder-shared'
} else if (mimeType === 'dir-public' && files.includes('folder-public')) {
return 'folder-public'
} else if ((mimeType === 'dir-external' || mimeType === 'dir-external-root') && files.includes('folder-external')) {
return 'folder-external'
} else if (files.includes(icon)) {
return icon
} else if (files.includes(icon.split('-')[0])) {
return icon.split('-')[0]
} else if (files.includes('file')) {
return 'file'
}
return null
}

View file

@ -0,0 +1,118 @@
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { join } from 'node:path'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const generateUrl = vi.hoisted(() => vi.fn((url) => join('/ROOT', url)))
vi.mock('@nextcloud/router', () => ({
generateUrl,
}))
beforeEach(() => {
vi.resetModules()
vi.resetAllMocks()
})
describe('OC.MimeType tests', async () => {
beforeEach(async () => {
window.OC.MimeTypeList = {
aliases: { 'app/foobar': 'foo/bar' },
files: ['folder', 'folder-shared', 'folder-external', 'foo-bar', 'foo', 'file'],
themes: {
abc: ['folder'],
},
}
})
describe('no theme', async () => {
beforeEach(async () => {
window.OC.theme ??= {}
window.OC.theme.folder = ''
})
it.for([
// returns the correct icon for a mimetype
{ mimeType: 'file', icon: 'file' },
{ mimeType: 'dir', icon: 'folder' },
{ mimeType: 'dir-shared', icon: 'folder-shared' },
{ mimeType: 'dir-external', icon: 'folder-external' },
// returns the correct icon for a mimetype for which we have an icon
{ mimeType: 'foo/bar', icon: 'foo-bar' },
// returns the correct icon for a mimetype for which we only have a general mimetype icon
{ mimeType: 'foo/baz', icon: 'foo' },
// return the file mimetype if we have no matching icon but do have a file icon
{ mimeType: 'foobar', icon: 'file' },
])('returns correct icon', async ({ icon, mimeType }) => {
const { getIconUrl } = await getMethod()
expect(getIconUrl(mimeType)).toEqual(`/ROOT/core/img/filetypes/${icon}.svg`)
})
it('returns undefined if the an icon for undefined is requested', async () => {
const { getIconUrl } = await getMethod()
expect(getIconUrl(undefined)).toEqual(undefined)
})
it('uses the cache if available', async () => {
const { getIconUrl } = await getMethod()
expect(generateUrl).not.toHaveBeenCalled()
expect(getIconUrl('dir')).toEqual('/ROOT/core/img/filetypes/folder.svg')
expect(generateUrl).toHaveBeenCalledTimes(1)
expect(getIconUrl('dir')).toEqual('/ROOT/core/img/filetypes/folder.svg')
expect(generateUrl).toHaveBeenCalledTimes(1)
expect(getIconUrl('dir-shared')).toEqual('/ROOT/core/img/filetypes/folder-shared.svg')
expect(generateUrl).toHaveBeenCalledTimes(2)
})
it('converts aliases correctly', async () => {
const { getIconUrl } = await getMethod()
expect(getIconUrl('app/foobar')).toEqual('/ROOT/core/img/filetypes/foo-bar.svg')
})
})
describe('with legacy themes', async () => {
beforeEach(async () => {
window.OC.theme ??= {}
window.OC.theme.folder = 'abc'
})
it('uses theme path if a theme icon is availble', async () => {
const { getIconUrl } = await getMethod()
expect(getIconUrl('dir')).toEqual('/ROOT/themes/abc/core/img/filetypes/folder.svg')
})
it('fallbacks to the default theme if no icon is available in the theme', async () => {
const { getIconUrl } = await getMethod()
expect(getIconUrl('dir-shared')).toEqual('/ROOT/core/img/filetypes/folder-shared.svg')
})
})
describe('with theming app', async () => {
beforeEach(async () => {
window.OC.theme ??= {}
window.OC.theme.folder = ''
window.OCA.Theming ??= {}
window.OCA.Theming.cacheBuster = '1cacheBuster2'
})
it('uses the correct theming URL', async () => {
const { getIconUrl } = await getMethod()
expect(getIconUrl('dir')).toMatch('/apps/theming/img/core/filetypes/folder.svg')
})
it('uses the cache buster', async () => {
const { getIconUrl } = await getMethod()
expect(getIconUrl('file')).toMatch(/\?v=1cacheBuster2$/)
})
})
})
async function getMethod() {
return await import('../../OC/mimeType.js')
}