Skip to content

Commit

Permalink
Merge pull request #917 from nextcloud-libraries/feat/update-files
Browse files Browse the repository at this point in the history
  • Loading branch information
skjnldsv authored Aug 22, 2023
2 parents 42a3613 + 20214d0 commit 9ce1400
Show file tree
Hide file tree
Showing 8 changed files with 1,204 additions and 23 deletions.
65 changes: 65 additions & 0 deletions .github/workflows/node-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
name: Node tests

on:
pull_request:
push:
branches:
- main
- master
- stable*

permissions:
contents: read

concurrency:
group: node-tests-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3

- name: Read package.json node and npm engines version
uses: skjnldsv/read-package-engines-version-actions@8205673bab74a63eb9b8093402fd9e0e018663a1 # v2.2
id: versions
with:
fallbackNode: '^20'
fallbackNpm: '^9'

- name: Set up node ${{ steps.versions.outputs.nodeVersion }}
uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1
with:
node-version: ${{ steps.versions.outputs.nodeVersion }}

- name: Set up npm ${{ steps.versions.outputs.npmVersion }}
run: npm i -g npm@"${{ steps.versions.outputs.npmVersion }}"

- name: Install dependencies & build
run: |
npm ci
npm run build --if-present
- name: Test
run: npm run test --if-present

- name: Test and process coverage
run: npm run test:coverage --if-present

- name: Collect coverage
uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # v3.1.4
with:
files: ./coverage/lcov.info

summary:
runs-on: ubuntu-latest
needs: test
if: always()

name: test-summary
steps:
- name: Summary status
run: if ${{ needs.test.result != 'success' && needs.test.result != 'skipped' }}; then exit 1; fi
199 changes: 199 additions & 0 deletions lib/usables/dav.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
/**
* @copyright Copyright (c) 2023 Ferdinand Thiessen <opensource@fthiessen.de>
*
* @author Ferdinand Thiessen <opensource@fthiessen.de>
*
* @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 <http://www.gnu.org/licenses/>.
*
*/

import { describe, it, expect, vi, afterEach } from 'vitest'
import { shallowMount } from '@vue/test-utils'
import { defineComponent, ref, toRef } from 'vue'
import { useDAVFiles } from './dav'

const nextcloudFiles = vi.hoisted(() => ({
davGetClient: vi.fn(),
davRootPath: '/root/uid',
davResultToNode: vi.fn(),
davGetDefaultPropfind: vi.fn(),
davGetRecentSearch: (time: number) => `recent ${time}`,
getFavoriteNodes: vi.fn(),
}))
vi.mock('@nextcloud/files', () => nextcloudFiles)

const waitLoaded = (vue: ReturnType<typeof shallowMount>) => new Promise((resolve) => {
const w = () => {
if (vue.vm.isLoading) window.setTimeout(w, 50)
else resolve(true)
}
w()
})

const TestComponent = defineComponent({
props: ['currentView', 'currentPath'],
setup(props) {
const dav = useDAVFiles(toRef(props, 'currentView'), toRef(props, 'currentPath'))
return {
...dav,
}
},
render: (h) => h('div'),
})

describe('dav usable', () => {
afterEach(() => { vi.resetAllMocks() })

it('Sets the inital state correctly', () => {
const client = {
getDirectoryContents: vi.fn(() => ({ data: [] })),
}
nextcloudFiles.davGetClient.mockImplementationOnce(() => client)

const vue = shallowMount(TestComponent, {
propsData: {
currentView: 'files',
currentPath: '/',
},
})
// Loading is set to true
expect(vue.vm.isLoading).toBe(true)
// Dav client for dav remote url is gathered
expect(nextcloudFiles.davGetClient).toBeCalled()
// files is an empty array
expect(Array.isArray(vue.vm.files)).toBe(true)
expect(vue.vm.files.length).toBe(0)
// functions
expect(typeof vue.vm.getFile === 'function').toBe(true)
expect(typeof vue.vm.loadFiles === 'function').toBe(true)
})

it('loads initial file list', async () => {
const client = {
getDirectoryContents: vi.fn(() => ({ data: ['1', '2'] })),
}
nextcloudFiles.davGetClient.mockImplementationOnce(() => client)
nextcloudFiles.davResultToNode.mockImplementation((v) => `node ${v}`)

const vue = shallowMount(TestComponent, {
propsData: {
currentView: 'files',
currentPath: '/',
},
})

// wait until files are loaded
await waitLoaded(vue)

expect(vue.vm.files).toEqual(['node 1', 'node 2'])
})

it('reloads on path change', async () => {
const client = {
getDirectoryContents: vi.fn((str) => ({ data: [] })),
}
nextcloudFiles.davGetClient.mockImplementationOnce(() => client)

const vue = shallowMount(TestComponent, {
propsData: {
currentView: 'files',
currentPath: '/',
},
})

// wait until files are loaded
await waitLoaded(vue)

expect(client.getDirectoryContents).toBeCalledTimes(1)
expect(client.getDirectoryContents.mock.calls[0][0]).toBe(`${nextcloudFiles.davRootPath}/`)

vue.setProps({ currentPath: '/other' })
await waitLoaded(vue)

expect(client.getDirectoryContents).toBeCalledTimes(2)
expect(client.getDirectoryContents.mock.calls[1][0]).toBe(`${nextcloudFiles.davRootPath}/other`)
})

it('reloads on view change', async () => {
const client = {
getDirectoryContents: vi.fn((str) => ({ data: [] })),
}
nextcloudFiles.davGetClient.mockImplementationOnce(() => client)

const vue = shallowMount(TestComponent, {
propsData: {
currentView: 'files',
currentPath: '/',
},
})

// wait until files are loaded
await waitLoaded(vue)

expect(client.getDirectoryContents).toBeCalledTimes(1)
expect(client.getDirectoryContents.mock.calls[0][0]).toBe(`${nextcloudFiles.davRootPath}/`)

vue.setProps({ currentView: 'recent' })
await waitLoaded(vue)

expect(client.getDirectoryContents).toBeCalledTimes(2)
})

it('getFile works', async () => {
const client = {
stat: vi.fn((v) => ({ data: { path: v } })),
getDirectoryContents: vi.fn(() => ({ data: [] })),
}
nextcloudFiles.davGetClient.mockImplementationOnce(() => client)
nextcloudFiles.davResultToNode.mockImplementationOnce((v) => v)

const { getFile } = useDAVFiles(ref('files'), ref('/'))

const node = await getFile('/some/path')
expect(node).toEqual({ path: `${nextcloudFiles.davRootPath}/some/path` })
expect(client.stat).toBeCalledWith(`${nextcloudFiles.davRootPath}/some/path`, { details: true })
expect(nextcloudFiles.davResultToNode).toBeCalledWith({ path: `${nextcloudFiles.davRootPath}/some/path` })
})

it('loadFiles work', async () => {
const client = {
stat: vi.fn((v) => ({ data: { path: v } })),
getDirectoryContents: vi.fn((p, o) => ({ data: [] })),
}
nextcloudFiles.davGetClient.mockImplementationOnce(() => client)
nextcloudFiles.davResultToNode.mockImplementationOnce((v) => v)

const view = ref<'files' | 'recent' | 'favorites'>('files')
const path = ref('/')
const { loadFiles, isLoading } = useDAVFiles(view, path)

expect(isLoading.value).toBe(true)
await loadFiles()
expect(isLoading.value).toBe(false)
expect(client.getDirectoryContents).toBeCalledWith(`${nextcloudFiles.davRootPath}/`, { details: true })

view.value = 'recent'
await loadFiles()
expect(isLoading.value).toBe(false)
expect(client.getDirectoryContents).toBeCalledWith(`${nextcloudFiles.davRootPath}/`, { details: true })
expect(client.getDirectoryContents.mock.calls.at(-1)?.[1]?.data).toMatch('recent')

view.value = 'favorites'
await loadFiles()
expect(isLoading.value).toBe(false)
expect(nextcloudFiles.getFavoriteNodes).toBeCalled()
})
})
31 changes: 15 additions & 16 deletions lib/usables/dav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,8 @@ import type { Node } from '@nextcloud/files'
import type { ComputedRef, Ref } from 'vue'
import type { FileStat, ResponseDataDetailed, WebDAVClient } from 'webdav'

import { davGetClient, davGetDefaultPropfind, davGetFavoritesReport, davGetRecentSearch, davResultToNode, davRootPath } from '@nextcloud/files'
import { generateRemoteUrl } from '@nextcloud/router'
import { ref, watch } from 'vue'
import { davGetClient, davGetDefaultPropfind, davGetRecentSearch, davResultToNode, davRootPath, getFavoriteNodes } from '@nextcloud/files'
import { onMounted, ref, watch } from 'vue'

/**
* Handle file loading using WebDAV
Expand All @@ -37,10 +36,10 @@ export const useDAVFiles = function(currentView: Ref<'files'|'recent'|'favorites
/**
* The WebDAV client
*/
const client = davGetClient(generateRemoteUrl('dav'))
const client = davGetClient()

/**
* All queried files
* All files in current view and path
*/
const files = ref<Node[]>([] as Node[]) as Ref<Node[]>

Expand All @@ -51,30 +50,25 @@ export const useDAVFiles = function(currentView: Ref<'files'|'recent'|'favorites

/**
* Get information for one file
*
* @param path The path of the file or folder
* @param rootPath DAV root path, defaults to '/files/USERID'
*/
async function getFile(path: string) {
const result = await client.stat(`${davRootPath}${path}`, {
async function getFile(path: string, rootPath = davRootPath) {
const result = await client.stat(`${rootPath}${path}`, {
details: true,
}) as ResponseDataDetailed<FileStat>
return davResultToNode(result.data)
}

/**
* Load files using the DAV client
* Force reload files using the DAV client
*/
async function loadDAVFiles() {
isLoading.value = true

if (currentView.value === 'favorites') {
files.value = await client.getDirectoryContents(`${davRootPath}${currentPath.value}`, {
details: true,
data: davGetFavoritesReport(),
headers: {
method: 'REPORT',
},
includeSelf: false,
}).then((result) => (result as ResponseDataDetailed<FileStat[]>).data.map((data) => davResultToNode(data)))
files.value = await getFavoriteNodes(client, currentPath.value)
} else if (currentView.value === 'recent') {
// unix timestamp in seconds, two weeks ago
const lastTwoWeek = Math.round(Date.now() / 1000) - (60 * 60 * 24 * 14)
Expand Down Expand Up @@ -105,6 +99,11 @@ export const useDAVFiles = function(currentView: Ref<'files'|'recent'|'favorites
*/
watch([currentView, currentPath], () => loadDAVFiles())

/**
* Initial loading of nodes
*/
onMounted(() => loadDAVFiles())

return {
isLoading,
files,
Expand Down
58 changes: 58 additions & 0 deletions lib/usables/mime.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* @copyright Copyright (c) 2023 Ferdinand Thiessen <opensource@fthiessen.de>
*
* @author Ferdinand Thiessen <opensource@fthiessen.de>
*
* @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 <http://www.gnu.org/licenses/>.
*
*/

import { describe, expect, it } from 'vitest'
import { useMimeFilter } from './mime'
import { ref } from 'vue'

describe('mime useable', () => {
it('isSupportedMimeType returns true if supported', () => {
const supported = ref(['text/plain'])
const { isSupportedMimeType } = useMimeFilter(supported)

expect(isSupportedMimeType('text/plain')).toBe(true)
})

it('isSupportedMimeType returns false if not supported', () => {
const supported = ref(['text/plain'])
const { isSupportedMimeType } = useMimeFilter(supported)

expect(isSupportedMimeType('font/truetype')).toBe(false)
})

it('isSupportedMimeType is reactive', () => {
const supported = ref(['text/plain'])
const { isSupportedMimeType } = useMimeFilter(supported)

expect(isSupportedMimeType('font/truetype')).toBe(false)
supported.value.push('font/truetype')
expect(isSupportedMimeType('font/truetype')).toBe(true)
})

it('isSupportedMimeType works with wildcards', () => {
const supported = ref(['text/*'])
const { isSupportedMimeType } = useMimeFilter(supported)

expect(isSupportedMimeType('text/plain')).toBe(true)
expect(isSupportedMimeType('font/truetype')).toBe(false)
})
})
Loading

0 comments on commit 9ce1400

Please sign in to comment.