test: remove testing internal of libraries and test only app code

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
This commit is contained in:
Ferdinand Thiessen 2026-02-10 01:48:18 +01:00
parent 8b27b1b826
commit 41b6f6012d
No known key found for this signature in database
GPG key ID: 7E849AE05218500F
5 changed files with 90 additions and 307 deletions

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { Folder, Navigation } from '@nextcloud/files'
import type { IFolder } from '@nextcloud/files'
import FolderSvg from '@mdi/svg/svg/folder.svg?raw'
import { getNavigation, View } from '@nextcloud/files'
@ -21,13 +21,21 @@ beforeAll(async () => {
await fireEvent.resize(window)
})
const navigation = getNavigation()
beforeEach(() => {
const views = [...navigation.views]
for (const view of views) {
navigation.remove(view.id)
}
expect(navigation.views).toHaveLength(0)
})
describe('Navigation', () => {
beforeEach(cleanup)
beforeEach(async () => {
delete window._nc_navigation
mockWindow()
getNavigation().register(createView('files', 'Files'))
navigation.register(createView('files', 'Files'))
await router.replace({ name: 'filelist', params: { view: 'files' } })
})
@ -130,11 +138,7 @@ describe('Navigation', () => {
})
describe('Navigation API', () => {
let Navigation: Navigation
beforeEach(async () => {
delete window._nc_navigation
Navigation = getNavigation()
mockWindow()
await router.replace({ name: 'filelist', params: { view: 'files' } })
@ -144,7 +148,7 @@ describe('Navigation API', () => {
beforeEach(cleanup)
it('Check API entries rendering', async () => {
Navigation.register(createView('files', 'Files'))
navigation.register(createView('files', 'Files'))
const component = render(NavigationView, {
router,
@ -171,8 +175,8 @@ describe('Navigation API', () => {
})
it('Adds a new entry and render', async () => {
Navigation.register(createView('files', 'Files'))
Navigation.register(createView('sharing', 'Sharing'))
navigation.register(createView('files', 'Files'))
navigation.register(createView('sharing', 'Sharing'))
const component = render(NavigationView, {
router,
@ -198,9 +202,9 @@ describe('Navigation API', () => {
})
it('Adds a new children, render and open menu', async () => {
Navigation.register(createView('files', 'Files'))
Navigation.register(createView('sharing', 'Sharing'))
Navigation.register(createView('sharingin', 'Shared with me', 'sharing'))
navigation.register(createView('files', 'Files'))
navigation.register(createView('sharing', 'Sharing'))
navigation.register(createView('sharingin', 'Shared with me', 'sharing'))
const component = render(NavigationView, {
router,
@ -272,7 +276,7 @@ function createView(id: string, name: string, parent?: string) {
return new View({
id,
name,
getContents: async () => ({ folder: {} as Folder, contents: [] }),
getContents: async () => ({ folder: {} as IFolder, contents: [] }),
icon: FolderSvg,
order: 1,
parent,

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { Navigation, Folder as NcFolder } from '@nextcloud/files'
import type { IFolder } from '@nextcloud/files'
import * as eventBus from '@nextcloud/event-bus'
import * as filesUtils from '@nextcloud/files'
@ -23,30 +23,26 @@ window.OC = {
TAG_FAVORITE: '_$!<Favorite>!$_',
}
declare global {
interface Window {
_nc_navigation?: Navigation
const navigation = getNavigation()
beforeEach(() => {
vi.resetAllMocks()
const views = [...navigation.views]
for (const view of views) {
navigation.remove(view.id)
}
}
expect(navigation.views).toHaveLength(0)
})
describe('Favorites view definition', () => {
let Navigation
beforeEach(() => {
vi.resetAllMocks()
delete window._nc_navigation
Navigation = getNavigation()
expect(window._nc_navigation).toBeDefined()
})
test('Default empty favorite view', async () => {
vi.spyOn(eventBus, 'subscribe')
vi.spyOn(filesDavUtils, 'getFavoriteNodes').mockReturnValue(Promise.resolve([]))
vi.spyOn(favoritesService, 'getContents').mockReturnValue(Promise.resolve({ folder: {} as NcFolder, contents: [] }))
vi.spyOn(favoritesService, 'getContents').mockReturnValue(Promise.resolve({ folder: {} as IFolder, contents: [] }))
await registerFavoritesView()
const favoritesView = Navigation.views.find((view) => view.id === 'favorites')
const favoriteFoldersViews = Navigation.views.filter((view) => view.parent === 'favorites')
const favoritesView = navigation.views.find((view) => view.id === 'favorites')
const favoriteFoldersViews = navigation.views.filter((view) => view.parent === 'favorites')
expect(eventBus.subscribe).toHaveBeenCalledTimes(3)
expect(eventBus.subscribe).toHaveBeenNthCalledWith(1, 'files:favorites:added', expect.anything())
@ -54,7 +50,7 @@ describe('Favorites view definition', () => {
expect(eventBus.subscribe).toHaveBeenNthCalledWith(3, 'files:node:renamed', expect.anything())
// one main view and no children
expect(Navigation.views.length).toBe(1)
expect(navigation.views.length).toBe(1)
expect(favoritesView).toBeDefined()
expect(favoriteFoldersViews.length).toBe(0)
@ -95,14 +91,14 @@ describe('Favorites view definition', () => {
}),
]
vi.spyOn(filesDavUtils, 'getFavoriteNodes').mockReturnValue(Promise.resolve(favoriteFolders))
vi.spyOn(favoritesService, 'getContents').mockReturnValue(Promise.resolve({ folder: {} as NcFolder, contents: favoriteFolders }))
vi.spyOn(favoritesService, 'getContents').mockReturnValue(Promise.resolve({ folder: {} as IFolder, contents: favoriteFolders }))
await registerFavoritesView()
const favoritesView = Navigation.views.find((view) => view.id === 'favorites')
const favoriteFoldersViews = Navigation.views.filter((view) => view.parent === 'favorites')
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(5)
expect(navigation.views.length).toBe(5)
expect(favoritesView).toBeDefined()
expect(favoriteFoldersViews.length).toBe(4)
@ -129,25 +125,17 @@ describe('Favorites view definition', () => {
})
describe('Dynamic update of favorite folders', () => {
let Navigation
beforeEach(() => {
vi.restoreAllMocks()
delete window._nc_navigation
Navigation = getNavigation()
})
test('Add a favorite folder creates a new entry in the navigation', async () => {
vi.spyOn(eventBus, 'emit')
vi.spyOn(filesDavUtils, 'getFavoriteNodes').mockReturnValue(Promise.resolve([]))
vi.spyOn(favoritesService, 'getContents').mockReturnValue(Promise.resolve({ folder: {} as NcFolder, contents: [] }))
vi.spyOn(favoritesService, 'getContents').mockReturnValue(Promise.resolve({ folder: {} as IFolder, contents: [] }))
await registerFavoritesView()
const favoritesView = Navigation.views.find((view) => view.id === 'favorites')
const favoriteFoldersViews = Navigation.views.filter((view) => view.parent === 'favorites')
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(navigation.views.length).toBe(1)
expect(favoritesView).toBeDefined()
expect(favoriteFoldersViews.length).toBe(0)
@ -162,8 +150,8 @@ describe('Dynamic update of favorite folders', () => {
// Exec the action
await action.exec({
nodes: [folder],
view: favoritesView,
folder: {} as NcFolder,
view: favoritesView!,
folder: {} as IFolder,
contents: [],
})
@ -181,14 +169,14 @@ describe('Dynamic update of favorite folders', () => {
})]
vi.spyOn(eventBus, 'emit')
vi.spyOn(filesDavUtils, 'getFavoriteNodes').mockReturnValue(Promise.resolve(favoriteFolders))
vi.spyOn(favoritesService, 'getContents').mockReturnValue(Promise.resolve({ folder: {} as NcFolder, contents: favoriteFolders }))
vi.spyOn(favoritesService, 'getContents').mockReturnValue(Promise.resolve({ folder: {} as IFolder, contents: favoriteFolders }))
await registerFavoritesView()
let favoritesView = Navigation.views.find((view) => view.id === 'favorites')
let favoriteFoldersViews = Navigation.views.filter((view) => view.parent === 'favorites')
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(navigation.views.length).toBe(2)
expect(favoritesView).toBeDefined()
expect(favoriteFoldersViews.length).toBe(1)
@ -209,8 +197,8 @@ describe('Dynamic update of favorite folders', () => {
// Exec the action
await action.exec({
nodes: [folder],
view: favoritesView,
folder: {} as NcFolder,
view: favoritesView!,
folder: {} as IFolder,
contents: [],
})
@ -219,11 +207,11 @@ describe('Dynamic update of favorite folders', () => {
expect(eventBus.emit).toHaveBeenCalledWith('files:node:updated', folder)
expect(fo).toHaveBeenCalled()
favoritesView = Navigation.views.find((view) => view.id === 'favorites')
favoriteFoldersViews = Navigation.views.filter((view) => view.parent === 'favorites')
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(navigation.views.length).toBe(1)
expect(favoritesView).toBeDefined()
expect(favoriteFoldersViews.length).toBe(0)
})
@ -231,14 +219,14 @@ describe('Dynamic update of favorite folders', () => {
test('Renaming a favorite folder updates the navigation', async () => {
vi.spyOn(eventBus, 'emit')
vi.spyOn(filesDavUtils, 'getFavoriteNodes').mockReturnValue(Promise.resolve([]))
vi.spyOn(favoritesService, 'getContents').mockReturnValue(Promise.resolve({ folder: {} as NcFolder, contents: [] }))
vi.spyOn(favoritesService, 'getContents').mockReturnValue(Promise.resolve({ folder: {} as IFolder, contents: [] }))
await registerFavoritesView()
const favoritesView = Navigation.views.find((view) => view.id === 'favorites')
const favoriteFoldersViews = Navigation.views.filter((view) => view.parent === 'favorites')
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(navigation.views.length).toBe(1)
expect(favoritesView).toBeDefined()
expect(favoriteFoldersViews.length).toBe(0)
@ -255,8 +243,8 @@ describe('Dynamic update of favorite folders', () => {
// Exec the action
await action.exec({
nodes: [folder],
view: favoritesView,
folder: {} as NcFolder,
view: favoritesView!,
folder: {} as IFolder,
contents: [],
})
expect(eventBus.emit).toHaveBeenCalledWith('files:favorites:added', folder)

View file

@ -3,44 +3,40 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { Navigation, View } from '@nextcloud/files'
import type { View } from '@nextcloud/files'
import type { OCSResponse } from '@nextcloud/typings/ocs'
import axios from '@nextcloud/axios'
import { Folder, getNavigation } from '@nextcloud/files'
import { getNavigation } from '@nextcloud/files'
import * as ncInitialState from '@nextcloud/initial-state'
import { beforeEach, describe, expect, test, vi } from 'vitest'
import registerSharingViews from './shares.ts'
import '../main.ts'
declare global {
interface Window {
_nc_navigation?: Navigation
const navigation = getNavigation()
beforeEach(() => {
vi.resetAllMocks()
const views = [...navigation.views]
for (const view of views) {
navigation.remove(view.id)
}
}
expect(navigation.views).toHaveLength(0)
})
describe('Sharing views definition', () => {
let Navigation
beforeEach(() => {
delete window._nc_navigation
Navigation = getNavigation()
expect(window._nc_navigation).toBeDefined()
})
test('Default values', () => {
vi.spyOn(Navigation, 'register')
expect(Navigation.views.length).toBe(0)
vi.spyOn(navigation, 'register')
registerSharingViews()
const shareOverviewView = Navigation.views.find((view) => view.id === 'shareoverview') as View
const sharesChildViews = Navigation.views.filter((view) => view.parent === 'shareoverview') as View[]
const shareOverviewView = navigation.views.find((view) => view.id === 'shareoverview') as View
const sharesChildViews = navigation.views.filter((view) => view.parent === 'shareoverview') as View[]
expect(Navigation.register).toHaveBeenCalledTimes(7)
expect(navigation.register).toHaveBeenCalledTimes(7)
// one main view and no children
expect(Navigation.views.length).toBe(7)
expect(navigation.views.length).toBe(7)
expect(shareOverviewView).toBeDefined()
expect(sharesChildViews.length).toBe(6)
@ -76,35 +72,28 @@ describe('Sharing views definition', () => {
})
test('Shared with others view is not registered if user has no storage quota', () => {
vi.spyOn(Navigation, 'register')
vi.spyOn(navigation, 'register')
const spy = vi.spyOn(ncInitialState, 'loadState').mockImplementationOnce(() => ({ quota: 0 }))
expect(Navigation.views.length).toBe(0)
expect(navigation.views.length).toBe(0)
registerSharingViews()
expect(Navigation.register).toHaveBeenCalledTimes(6)
expect(Navigation.views.length).toBe(6)
expect(navigation.register).toHaveBeenCalledTimes(6)
expect(navigation.views.length).toBe(6)
const shareOverviewView = Navigation.views.find((view) => view.id === 'shareoverview') as View
const sharesChildViews = Navigation.views.filter((view) => view.parent === 'shareoverview') as View[]
const shareOverviewView = navigation.views.find((view) => view.id === 'shareoverview') as View
const sharesChildViews = navigation.views.filter((view) => view.parent === 'shareoverview') as View[]
expect(shareOverviewView).toBeDefined()
expect(sharesChildViews.length).toBe(5)
expect(spy).toHaveBeenCalled()
expect(spy).toHaveBeenCalledWith('files', 'storageStats', { quota: -1 })
const sharedWithOthersView = Navigation.views.find((view) => view.id === 'sharingout')
const sharedWithOthersView = navigation.views.find((view) => view.id === 'sharingout')
expect(sharedWithOthersView).toBeUndefined()
})
})
describe('Sharing views contents', () => {
let Navigation
beforeEach(() => {
delete window._nc_navigation
Navigation = getNavigation()
expect(window._nc_navigation).toBeDefined()
})
test('Sharing overview get contents', async () => {
vi.spyOn(axios, 'get').mockImplementation(async (): Promise<any> => {
return {
@ -122,11 +111,11 @@ describe('Sharing views contents', () => {
})
registerSharingViews()
expect(Navigation.views.length).toBe(7)
Navigation.views.forEach(async (view: View) => {
const content = await view.getContents('/')
expect(navigation.views.length).toBe(7)
for (const view of navigation.views) {
const content = await view.getContents('/', { signal: new AbortController().signal })
expect(content.contents).toStrictEqual([])
expect(content.folder).toBeInstanceOf(Folder)
})
expect(content.folder).toBeTypeOf('object')
}
})
})

View file

@ -170,13 +170,13 @@ describe('files_trashbin: file actions - restore action', () => {
})
it('does not delete node from view if request failed', async () => {
const node = new Folder({ owner: 'test', source: 'https://example.com/remote.php/dav/trashbin/test/folder', root: '/trashbin/test/', permissions: PERMISSION_ALL })
vi.spyOn(window.console, 'error').mockImplementation(() => {})
const emitSpy = vi.spyOn(ncEventBus, 'emit')
axiosMock.request.mockImplementationOnce(() => {
throw new Error()
})
const emitSpy = vi.spyOn(ncEventBus, 'emit')
const node = new Folder({ owner: 'test', source: 'https://example.com/remote.php/dav/trashbin/test/folder', root: '/trashbin/test/', permissions: PERMISSION_ALL })
expect(await restoreAction.exec({
nodes: [node],
view: trashbinView,
@ -189,6 +189,7 @@ describe('files_trashbin: file actions - restore action', () => {
})
it('batch: only returns success if all requests worked', async () => {
vi.spyOn(window.console, 'error').mockImplementation(() => {})
const node = new Folder({ owner: 'test', source: 'https://example.com/remote.php/dav/trashbin/test/folder', root: '/trashbin/test/', permissions: PERMISSION_ALL })
expect(await restoreAction.execBatch!({
@ -201,6 +202,7 @@ describe('files_trashbin: file actions - restore action', () => {
})
it('batch: only returns success if all requests worked - one failed', async () => {
vi.spyOn(window.console, 'error').mockImplementation(() => {})
const node = new Folder({ owner: 'test', source: 'https://example.com/remote.php/dav/trashbin/test/folder', root: '/trashbin/test/', permissions: PERMISSION_ALL })
axiosMock.request.mockImplementationOnce(() => {

View file

@ -4,7 +4,6 @@
*/
import type { User } from '@nextcloud/e2e-test-server/cypress'
import type { IFileAction } from '@nextcloud/files'
import { getActionButtonForFileId, getActionEntryForFileId, getRowForFile, getSelectionActionButton, getSelectionActionEntry, selectRowForFile } from './FilesUtils.ts'
@ -12,12 +11,6 @@ const ACTION_DELETE = 'delete'
const ACTION_COPY_MOVE = 'move-copy'
const ACTION_DETAILS = 'details'
declare global {
interface Window {
_nc_fileactions: IFileAction[]
}
}
// Those two arrays doesn't represent the full list of actions
// the goal is to test a few, we're not trying to match the full feature set
const expectedDefaultActionsIDs = [
@ -57,77 +50,6 @@ describe('Files: Actions', { testIsolation: true }, () => {
})
})
it('Show some nested actions', () => {
const parent: IFileAction = {
id: 'nested-action',
displayName: () => 'Nested Action',
exec: cy.spy(),
iconSvgInline: () => '<svg></svg>',
}
const child1: IFileAction = {
id: 'nested-child-1',
displayName: () => 'Nested Child 1',
exec: cy.spy(),
iconSvgInline: () => '<svg></svg>',
parent: 'nested-action',
}
const child2: IFileAction = {
id: 'nested-child-2',
displayName: () => 'Nested Child 2',
exec: cy.spy(),
iconSvgInline: () => '<svg></svg>',
parent: 'nested-action',
}
cy.visit('/apps/files', {
// Cannot use registerFileAction here
onBeforeLoad: (win) => {
if (!win._nc_fileactions) {
win._nc_fileactions = []
}
// Cannot use registerFileAction here
win._nc_fileactions.push(parent)
win._nc_fileactions.push(child1)
win._nc_fileactions.push(child2)
},
})
// Open the menu
getActionButtonForFileId(fileId)
.scrollIntoView()
.click({ force: true })
// Check we have the parent action but not the children
getActionEntryForFileId(fileId, 'nested-action').should('be.visible')
getActionEntryForFileId(fileId, 'menu-back').should('not.exist')
getActionEntryForFileId(fileId, 'nested-child-1').should('not.exist')
getActionEntryForFileId(fileId, 'nested-child-2').should('not.exist')
// Click on the parent action
getActionEntryForFileId(fileId, 'nested-action')
.should('be.visible')
.click()
// Check we have the children and the back button but not the parent
getActionEntryForFileId(fileId, 'nested-action').should('not.exist')
getActionEntryForFileId(fileId, 'menu-back').should('be.visible')
getActionEntryForFileId(fileId, 'nested-child-1').should('be.visible')
getActionEntryForFileId(fileId, 'nested-child-2').should('be.visible')
// Click on the back button
getActionEntryForFileId(fileId, 'menu-back')
.should('be.visible')
.click()
// Check we have the parent action but not the children
getActionEntryForFileId(fileId, 'nested-action').should('be.visible')
getActionEntryForFileId(fileId, 'menu-back').should('not.exist')
getActionEntryForFileId(fileId, 'nested-child-1').should('not.exist')
getActionEntryForFileId(fileId, 'nested-child-2').should('not.exist')
})
it('Show some actions for a selection', () => {
cy.visit('/apps/files')
getRowForFile('image.jpg').should('be.visible')
@ -145,126 +67,4 @@ describe('Files: Actions', { testIsolation: true }, () => {
getSelectionActionEntry(actionId).should('be.visible')
})
})
it('Show some nested actions for a selection', () => {
const parent: IFileAction = {
id: 'nested-action',
displayName: () => 'Nested Action',
exec: cy.spy(),
iconSvgInline: () => '<svg></svg>',
}
const child1: IFileAction = {
id: 'nested-child-1',
displayName: () => 'Nested Child 1',
exec: cy.spy(),
execBatch: cy.spy(),
iconSvgInline: () => '<svg></svg>',
parent: 'nested-action',
}
const child2: IFileAction = {
id: 'nested-child-2',
displayName: () => 'Nested Child 2',
exec: cy.spy(),
execBatch: cy.spy(),
iconSvgInline: () => '<svg></svg>',
parent: 'nested-action',
}
cy.visit('/apps/files', {
// Cannot use registerFileAction here
onBeforeLoad: (win) => {
if (!win._nc_fileactions) {
win._nc_fileactions = []
}
// Cannot use registerFileAction here
win._nc_fileactions.push(parent)
win._nc_fileactions.push(child1)
win._nc_fileactions.push(child2)
},
})
selectRowForFile('image.jpg')
// Open the menu
getSelectionActionButton().click({ force: true })
// Check we have the parent action but not the children
getSelectionActionEntry('nested-action').should('be.visible')
getSelectionActionEntry('menu-back').should('not.exist')
getSelectionActionEntry('nested-child-1').should('not.exist')
getSelectionActionEntry('nested-child-2').should('not.exist')
// Click on the parent action
getSelectionActionEntry('nested-action')
.find('button').last()
.should('exist').click({ force: true })
// Check we have the children and the back button but not the parent
getSelectionActionEntry('nested-action').should('not.exist')
getSelectionActionEntry('menu-back').should('be.visible')
getSelectionActionEntry('nested-child-1').should('be.visible')
getSelectionActionEntry('nested-child-2').should('be.visible')
// Click on the back button
getSelectionActionEntry('menu-back')
.find('button').last()
.should('exist').click({ force: true })
// Check we have the parent action but not the children
getSelectionActionEntry('nested-action').should('be.visible')
getSelectionActionEntry('menu-back').should('not.exist')
getSelectionActionEntry('nested-child-1').should('not.exist')
getSelectionActionEntry('nested-child-2').should('not.exist')
})
it('Do not show parent if nested action has no batch support', () => {
const parent: IFileAction = {
id: 'nested-action',
displayName: () => 'Nested Action',
exec: cy.spy(),
iconSvgInline: () => '<svg></svg>',
}
const child1: IFileAction = {
id: 'nested-child-1',
displayName: () => 'Nested Child 1',
exec: cy.spy(),
iconSvgInline: () => '<svg></svg>',
parent: 'nested-action',
}
const child2: IFileAction = {
id: 'nested-child-2',
displayName: () => 'Nested Child 2',
exec: cy.spy(),
iconSvgInline: () => '<svg></svg>',
parent: 'nested-action',
}
cy.visit('/apps/files', {
// Cannot use registerFileAction here
onBeforeLoad: (win) => {
if (!win._nc_fileactions) {
win._nc_fileactions = []
}
// Cannot use registerFileAction here
win._nc_fileactions.push(parent)
win._nc_fileactions.push(child1)
win._nc_fileactions.push(child2)
},
})
selectRowForFile('image.jpg')
// Open the menu
getSelectionActionButton().click({ force: true })
// Check we have the parent action but not the children
getSelectionActionEntry('nested-action').should('not.exist')
getSelectionActionEntry('menu-back').should('not.exist')
getSelectionActionEntry('nested-child-1').should('not.exist')
getSelectionActionEntry('nested-child-2').should('not.exist')
})
})