From 047218b5b08089c8455a37f9983ccb7047a10d25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20Molakvo=C3=A6?= Date: Fri, 30 Jun 2023 10:47:30 +0200 Subject: [PATCH] feat: add favorites view testing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: John Molakvoæ --- __mocks__/svg.js | 2 +- __mocks__/webdav.ts | 27 +++ apps/files/src/actions/deleteAction.spec.ts | 2 +- apps/files/src/actions/downloadAction.spec.ts | 2 +- .../src/actions/editLocallyAction.spec.ts | 2 +- apps/files/src/actions/favoriteAction.spec.ts | 2 +- .../src/actions/openFolderAction.spec.ts | 2 +- apps/files/src/actions/renameAction.spec.ts | 2 +- apps/files/src/actions/sidebarAction.spec.ts | 2 +- apps/files/src/services/DavProperties.ts | 2 + apps/files/src/services/Favorites.ts | 19 +- apps/files/src/views/favorites.spec.ts | 193 ++++++++++++++++++ apps/files/src/views/favorites.ts | 11 +- 13 files changed, 249 insertions(+), 19 deletions(-) create mode 100644 __mocks__/webdav.ts create mode 100644 apps/files/src/views/favorites.spec.ts diff --git a/__mocks__/svg.js b/__mocks__/svg.js index 52b1041f754..1afe4357c0d 100644 --- a/__mocks__/svg.js +++ b/__mocks__/svg.js @@ -19,4 +19,4 @@ * along with this program. If not, see . * */ -export default 'SvgMock' +export default 'SvgMock' diff --git a/__mocks__/webdav.ts b/__mocks__/webdav.ts new file mode 100644 index 00000000000..5a1498c0e2d --- /dev/null +++ b/__mocks__/webdav.ts @@ -0,0 +1,27 @@ +/** + * @copyright Copyright (c) 2023 John Molakvoæ + * + * @author John Molakvoæ + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +export const createClient = () => {} +export const getPatcher = () => { + return { + patch: () => {} + } +} diff --git a/apps/files/src/actions/deleteAction.spec.ts b/apps/files/src/actions/deleteAction.spec.ts index e45ef3d11c2..8d99b195c3d 100644 --- a/apps/files/src/actions/deleteAction.spec.ts +++ b/apps/files/src/actions/deleteAction.spec.ts @@ -43,7 +43,7 @@ describe('Delete action conditions tests', () => { expect(action).toBeInstanceOf(FileAction) expect(action.id).toBe('delete') expect(action.displayName([], view)).toBe('Delete') - expect(action.iconSvgInline([], view)).toBe('SvgMock') + expect(action.iconSvgInline([], view)).toBe('SvgMock') expect(action.default).toBeUndefined() expect(action.order).toBe(100) }) diff --git a/apps/files/src/actions/downloadAction.spec.ts b/apps/files/src/actions/downloadAction.spec.ts index 70f8f707099..abe099af3f8 100644 --- a/apps/files/src/actions/downloadAction.spec.ts +++ b/apps/files/src/actions/downloadAction.spec.ts @@ -38,7 +38,7 @@ describe('Download action conditions tests', () => { expect(action).toBeInstanceOf(FileAction) expect(action.id).toBe('download') expect(action.displayName([], view)).toBe('Download') - expect(action.iconSvgInline([], view)).toBe('SvgMock') + expect(action.iconSvgInline([], view)).toBe('SvgMock') expect(action.default).toBeUndefined() expect(action.order).toBe(30) }) diff --git a/apps/files/src/actions/editLocallyAction.spec.ts b/apps/files/src/actions/editLocallyAction.spec.ts index 7c5bed53391..af3e2bf7553 100644 --- a/apps/files/src/actions/editLocallyAction.spec.ts +++ b/apps/files/src/actions/editLocallyAction.spec.ts @@ -37,7 +37,7 @@ describe('Edit locally action conditions tests', () => { expect(action).toBeInstanceOf(FileAction) expect(action.id).toBe('edit-locally') expect(action.displayName([], view)).toBe('Edit locally') - expect(action.iconSvgInline([], view)).toBe('SvgMock') + expect(action.iconSvgInline([], view)).toBe('SvgMock') expect(action.default).toBe(DefaultType.DEFAULT) expect(action.order).toBe(25) }) diff --git a/apps/files/src/actions/favoriteAction.spec.ts b/apps/files/src/actions/favoriteAction.spec.ts index 1a026a6cc39..48a00094a0d 100644 --- a/apps/files/src/actions/favoriteAction.spec.ts +++ b/apps/files/src/actions/favoriteAction.spec.ts @@ -55,7 +55,7 @@ describe('Favorite action conditions tests', () => { expect(action).toBeInstanceOf(FileAction) expect(action.id).toBe('favorite') expect(action.displayName([file], view)).toBe('Add to favorites') - expect(action.iconSvgInline([], view)).toBe('SvgMock') + expect(action.iconSvgInline([], view)).toBe('SvgMock') expect(action.default).toBeUndefined() expect(action.order).toBe(-50) }) diff --git a/apps/files/src/actions/openFolderAction.spec.ts b/apps/files/src/actions/openFolderAction.spec.ts index 96c117c6229..5a0ccc98978 100644 --- a/apps/files/src/actions/openFolderAction.spec.ts +++ b/apps/files/src/actions/openFolderAction.spec.ts @@ -42,7 +42,7 @@ describe('Open folder action conditions tests', () => { expect(action).toBeInstanceOf(FileAction) expect(action.id).toBe('open-folder') expect(action.displayName([folder], view)).toBe('Open folder FooBar') - expect(action.iconSvgInline([], view)).toBe('SvgMock') + expect(action.iconSvgInline([], view)).toBe('SvgMock') expect(action.default).toBe(DefaultType.HIDDEN) expect(action.order).toBe(-100) }) diff --git a/apps/files/src/actions/renameAction.spec.ts b/apps/files/src/actions/renameAction.spec.ts index 4b44a32a0d0..c4d5d45cde9 100644 --- a/apps/files/src/actions/renameAction.spec.ts +++ b/apps/files/src/actions/renameAction.spec.ts @@ -36,7 +36,7 @@ describe('Rename action conditions tests', () => { expect(action).toBeInstanceOf(FileAction) expect(action.id).toBe('rename') expect(action.displayName([], view)).toBe('Rename') - expect(action.iconSvgInline([], view)).toBe('SvgMock') + expect(action.iconSvgInline([], view)).toBe('SvgMock') expect(action.default).toBeUndefined() expect(action.order).toBe(10) }) diff --git a/apps/files/src/actions/sidebarAction.spec.ts b/apps/files/src/actions/sidebarAction.spec.ts index 133c6a4f6b5..6c6c5b140e8 100644 --- a/apps/files/src/actions/sidebarAction.spec.ts +++ b/apps/files/src/actions/sidebarAction.spec.ts @@ -36,7 +36,7 @@ describe('Open sidebar action conditions tests', () => { expect(action).toBeInstanceOf(FileAction) expect(action.id).toBe('details') expect(action.displayName([], view)).toBe('Open details') - expect(action.iconSvgInline([], view)).toBe('SvgMock') + expect(action.iconSvgInline([], view)).toBe('SvgMock') expect(action.default).toBe(DefaultType.DEFAULT) expect(action.order).toBe(-50) }) diff --git a/apps/files/src/services/DavProperties.ts b/apps/files/src/services/DavProperties.ts index 598807511ca..79a80706925 100644 --- a/apps/files/src/services/DavProperties.ts +++ b/apps/files/src/services/DavProperties.ts @@ -63,6 +63,8 @@ const defaultDavNamespaces = { /** * TODO: remove and move to @nextcloud/files + * @param prop + * @param namespace */ export const registerDavProperty = function(prop: string, namespace: DavProperty = { nc: 'http://nextcloud.org/ns' }): void { if (typeof window._nc_dav_properties === 'undefined') { diff --git a/apps/files/src/services/Favorites.ts b/apps/files/src/services/Favorites.ts index 3837bb221b5..75b5580c555 100644 --- a/apps/files/src/services/Favorites.ts +++ b/apps/files/src/services/Favorites.ts @@ -25,7 +25,7 @@ import { getClient, rootPath } from './WebdavClient' import { getCurrentUser } from '@nextcloud/auth' import { getDavNameSpaces, getDavProperties, getDefaultPropfind } from './DavProperties' import type { ContentsWithRoot } from './Navigation' -import type { FileStat, ResponseDataDetailed } from 'webdav' +import type { FileStat, ResponseDataDetailed, DAVResultResponseProps } from 'webdav' const client = getClient() @@ -39,23 +39,30 @@ const reportPayload = ` ` +interface ResponseProps extends DAVResultResponseProps { + permissions: string, + fileid: number, + size: number, +} + const resultToNode = function(node: FileStat): File | Folder { - const permissions = parseWebdavPermissions(node.props?.permissions) + const props = node.props as ResponseProps + const permissions = parseWebdavPermissions(props?.permissions) const owner = getCurrentUser()?.uid as string - const previewUrl = generateUrl('/core/preview?fileId={fileid}&x=32&y=32&forceIcon=0', node.props) + const previewUrl = generateUrl('/core/preview?fileId={fileid}&x=32&y=32&forceIcon=0', props) const nodeData = { - id: node.props?.fileid as number || 0, + id: props?.fileid as number || 0, source: generateRemoteUrl('dav' + rootPath + node.filename), mtime: new Date(node.lastmod), mime: node.mime as string, - size: node.props?.size as number || 0, + size: props?.size as number || 0, permissions, owner, root: rootPath, attributes: { ...node, - ...node.props, + ...props, previewUrl, }, } diff --git a/apps/files/src/views/favorites.spec.ts b/apps/files/src/views/favorites.spec.ts new file mode 100644 index 00000000000..a1999624f2f --- /dev/null +++ b/apps/files/src/views/favorites.spec.ts @@ -0,0 +1,193 @@ +/** + * @copyright Copyright (c) 2023 John Molakvoæ + * + * @author John Molakvoæ + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +import { expect } from '@jest/globals' +import * as initialState from '@nextcloud/initial-state' +import { Folder } from '@nextcloud/files' +import { basename } from 'path' +import * as eventBus from '@nextcloud/event-bus' + +import { action } from '../actions/favoriteAction' +import * as favoritesService from '../services/Favorites' +import NavigationService from '../services/Navigation' +import registerFavoritesView from './favorites' + +jest.mock('webdav/dist/node/request.js', () => ({ + request: jest.fn(), +})) + +global.window.OC = { + TAG_FAVORITE: '_$!!$_', +} + +describe('Favorites view definition', () => { + let Navigation + beforeEach(() => { + Navigation = new NavigationService() + window.OCP = { Files: { Navigation } } + }) + + afterAll(() => { + delete window.OCP + }) + + test('Default empty favorite view', () => { + jest.spyOn(eventBus, 'subscribe') + jest.spyOn(favoritesService, 'getContents').mockReturnValue(Promise.resolve({ folder: {} as Folder, contents: [] })) + + registerFavoritesView() + const favoritesView = Navigation.views.find(view => view.id === 'favorites') + const favoriteFoldersViews = Navigation.views.filter(view => view.parent === 'favorites') + + expect(eventBus.subscribe).toHaveBeenCalledTimes(2) + expect(eventBus.subscribe).toHaveBeenNthCalledWith(1, 'files:favorites:added', expect.anything()) + expect(eventBus.subscribe).toHaveBeenNthCalledWith(2, 'files:favorites:removed', expect.anything()) + + // one main view and no children + expect(Navigation.views.length).toBe(1) + expect(favoritesView).toBeDefined() + expect(favoriteFoldersViews.length).toBe(0) + + expect(favoritesView?.id).toBe('favorites') + expect(favoritesView?.name).toBe('Favorites') + expect(favoritesView?.caption).toBe('List of favorites files and folders.') + expect(favoritesView?.icon).toBe('SvgMock') + expect(favoritesView?.order).toBe(5) + expect(favoritesView?.columns).toStrictEqual([]) + expect(favoritesView?.getContents).toBeDefined() + }) + + test('Default with favorites', () => { + const favoriteFolders = [ + '/foo', + '/bar', + '/foo/bar', + ] + jest.spyOn(initialState, 'loadState').mockReturnValue(favoriteFolders) + jest.spyOn(favoritesService, 'getContents').mockReturnValue(Promise.resolve({ folder: {} as Folder, contents: [] })) + + registerFavoritesView() + const favoritesView = Navigation.views.find(view => view.id === 'favorites') + const favoriteFoldersViews = Navigation.views.filter(view => view.parent === 'favorites') + + // one main view and 3 children + expect(Navigation.views.length).toBe(4) + expect(favoritesView).toBeDefined() + expect(favoriteFoldersViews.length).toBe(3) + + favoriteFolders.forEach((folder, index) => { + const favoriteView = favoriteFoldersViews[index] + expect(favoriteView).toBeDefined() + expect(favoriteView?.id).toBeDefined() + expect(favoriteView?.name).toBe(basename(folder)) + expect(favoriteView?.icon).toBe('SvgMock') + expect(favoriteView?.order).toBe(index) + expect(favoriteView?.params).toStrictEqual({ + dir: folder, + view: 'favorites', + }) + expect(favoriteView?.parent).toBe('favorites') + expect(favoriteView?.columns).toStrictEqual([]) + expect(favoriteView?.getContents).toBeDefined() + }) + }) +}) + +describe('Dynamic update of favourite folders', () => { + let Navigation + beforeEach(() => { + Navigation = new NavigationService() + window.OCP = { Files: { Navigation } } + }) + + afterAll(() => { + delete window.OCP + }) + + test('Add a favorite folder creates a new entry in the navigation', async () => { + jest.spyOn(eventBus, 'emit') + jest.spyOn(initialState, 'loadState').mockReturnValue([]) + jest.spyOn(favoritesService, 'getContents').mockReturnValue(Promise.resolve({ folder: {} as Folder, contents: [] })) + + registerFavoritesView() + const favoritesView = Navigation.views.find(view => view.id === 'favorites') + const favoriteFoldersViews = Navigation.views.filter(view => view.parent === 'favorites') + + // one main view and no children + expect(Navigation.views.length).toBe(1) + expect(favoritesView).toBeDefined() + expect(favoriteFoldersViews.length).toBe(0) + + // Create new folder to favorite + const folder = new Folder({ + id: 1, + source: 'http://localhost/remote.php/dav/files/admin/Foo/Bar', + owner: 'admin', + }) + + // Exec the action + await action.exec(folder, favoritesView, '/') + + expect(eventBus.emit).toHaveBeenCalledTimes(1) + expect(eventBus.emit).toHaveBeenCalledWith('files:favorites:added', folder) + }) + + test('Remove a favorite folder remove the entry from the navigation column', async () => { + jest.spyOn(eventBus, 'emit') + jest.spyOn(eventBus, 'subscribe') + jest.spyOn(initialState, 'loadState').mockReturnValue(['/Foo/Bar']) + jest.spyOn(favoritesService, 'getContents').mockReturnValue(Promise.resolve({ folder: {} as Folder, contents: [] })) + + registerFavoritesView() + let favoritesView = Navigation.views.find(view => view.id === 'favorites') + let favoriteFoldersViews = Navigation.views.filter(view => view.parent === 'favorites') + + // one main view and no children + expect(Navigation.views.length).toBe(2) + expect(favoritesView).toBeDefined() + expect(favoriteFoldersViews.length).toBe(1) + + // Create new folder to favorite + const folder = new Folder({ + id: 1, + source: 'http://localhost/remote.php/dav/files/admin/Foo/Bar', + owner: 'admin', + root: '/files/admin', + attributes: { + favorite: 1, + }, + }) + + // Exec the action + await action.exec(folder, favoritesView, '/') + + expect(eventBus.emit).toHaveBeenCalledTimes(1) + expect(eventBus.emit).toHaveBeenCalledWith('files:favorites:removed', folder) + + favoritesView = Navigation.views.find(view => view.id === 'favorites') + favoriteFoldersViews = Navigation.views.filter(view => view.parent === 'favorites') + + // one main view and no children + expect(Navigation.views.length).toBe(1) + expect(favoritesView).toBeDefined() + expect(favoriteFoldersViews.length).toBe(0) + }) +}) diff --git a/apps/files/src/views/favorites.ts b/apps/files/src/views/favorites.ts index e445731b370..86424f1785a 100644 --- a/apps/files/src/views/favorites.ts +++ b/apps/files/src/views/favorites.ts @@ -33,7 +33,7 @@ import { Node, FileType } from '@nextcloud/files' import { subscribe } from '@nextcloud/event-bus' import logger from '../logger' -const generateFolderView = function(folder: string, index = 0): Navigation { +export const generateFolderView = function(folder: string, index = 0): Navigation { return { id: generateIdFromPath(folder), name: basename(folder), @@ -53,14 +53,15 @@ const generateFolderView = function(folder: string, index = 0): Navigation { } as Navigation } -const generateIdFromPath = function(path: string): string { +export const generateIdFromPath = function(path: string): string { return `favorite-${hashCode(path)}` } -const favoriteFolders = loadState('files', 'favoriteFolders', []) as string[] -const favoriteFoldersViews = favoriteFolders.map((folder, index) => generateFolderView(folder, index)) - export default () => { + // Load state in function for mock testing purposes + const favoriteFolders = loadState('files', 'favoriteFolders', []) as string[] + const favoriteFoldersViews = favoriteFolders.map((folder, index) => generateFolderView(folder, index)) + const Navigation = window.OCP.Files.Navigation as NavigationService Navigation.register({ id: 'favorites',