Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions changelog/unreleased/change-web-app-store-unit-tests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Change: Add unit tests for the web app store views and stores

We've added unit test coverage for the web app store, covering the apps and
repositories pinia stores and the app list overview (no-content state, tile
rendering, search filtering and reflecting the search term into the route query).

https://github.com/owncloud/web/issues/11634
https://github.com/owncloud/ocis/pull/12545
102 changes: 102 additions & 0 deletions web/packages/web-app-app-store/tests/unit/piniaStores/apps.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { setActivePinia } from 'pinia'
import { createTestingPinia } from '@ownclouders/web-test-helpers'
import { useAppsStore } from '../../../src/piniaStores/apps'
import { useRepositoriesStore } from '../../../src/piniaStores/repositories'
import { AppStoreRepository } from '../../../src/types'

const repo1: AppStoreRepository = { name: 'repo1', url: 'https://example.test/repo1.json' }
const repo2: AppStoreRepository = { name: 'repo2', url: 'https://example.test/repo2.json' }

const makeRawApp = (id: string, name: string) => ({
id,
name,
subtitle: `${name} subtitle`,
license: 'MIT',
versions: [{ version: '1.0.0', url: `https://example.test/${id}.zip` }],
authors: [{ name: 'ownCloud' }],
tags: ['productivity']
})

const stubFetch = (responsesByUrl: Record<string, unknown | 'reject'>) => {
vi.stubGlobal(
'fetch',
vi.fn((url: string) => {
const entry = responsesByUrl[url]
if (entry === 'reject') {
return Promise.reject(new Error('network error'))
}
return Promise.resolve({ json: () => Promise.resolve(entry) })
})
)
}

describe('useAppsStore', () => {
beforeEach(() => {
setActivePinia(createTestingPinia({ stubActions: false }))
})

afterEach(() => {
vi.unstubAllGlobals()
})

describe('method "loadApps"', () => {
it('loads and maps apps from the configured repositories', async () => {
useRepositoriesStore().setRepositories([repo1])
stubFetch({ [repo1.url]: { apps: [makeRawApp('alpha', 'Alpha')] } })

const store = useAppsStore()
await store.loadApps()

expect(store.apps).toHaveLength(1)
expect(store.apps[0].id).toBe('alpha')
expect(store.apps[0].repository).toEqual(repo1)
expect(store.apps[0].mostRecentVersion).toEqual({
version: '1.0.0',
url: 'https://example.test/alpha.zip'
})
})

it('catches and logs a per-repository failure and returns no apps for that repository', async () => {
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined)
useRepositoriesStore().setRepositories([repo1, repo2])
stubFetch({
[repo1.url]: { apps: [makeRawApp('alpha', 'Alpha')] },
[repo2.url]: 'reject'
})

const store = useAppsStore()
await store.loadApps()

expect(errorSpy).toHaveBeenCalled()
expect(store.apps).toHaveLength(1)
expect(store.apps[0].id).toBe('alpha')
})

it('sorts the apps of a repository by name (case-insensitive)', async () => {
useRepositoriesStore().setRepositories([repo1])
stubFetch({
[repo1.url]: {
apps: [makeRawApp('c', 'Charlie'), makeRawApp('a', 'alpha'), makeRawApp('b', 'Bravo')]
}
})

const store = useAppsStore()
await store.loadApps()

expect(store.apps.map((app) => app.name)).toEqual(['alpha', 'Bravo', 'Charlie'])
})
})

describe('method "getById"', () => {
it('returns the matching app or undefined', async () => {
useRepositoriesStore().setRepositories([repo1])
stubFetch({ [repo1.url]: { apps: [makeRawApp('alpha', 'Alpha')] } })

const store = useAppsStore()
await store.loadApps()

expect(store.getById('alpha')?.name).toBe('Alpha')
expect(store.getById('does-not-exist')).toBeUndefined()
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { setActivePinia } from 'pinia'
import { createTestingPinia } from '@ownclouders/web-test-helpers'
import { useRepositoriesStore } from '../../../src/piniaStores/repositories'
import { AppStoreRepository } from '../../../src/types'

describe('useRepositoriesStore', () => {
beforeEach(() => {
setActivePinia(createTestingPinia({ stubActions: false }))
})

describe('method "setRepositories"', () => {
it('updates the repositories state', () => {
const store = useRepositoriesStore()
expect(store.repositories).toEqual([])

const repositories: AppStoreRepository[] = [
{ name: 'repo1', url: 'https://example.test/repo1.json' },
{ name: 'repo2', url: 'https://example.test/repo2.json' }
]
store.setRepositories(repositories)

expect(store.repositories).toEqual(repositories)
})
})
})
100 changes: 100 additions & 0 deletions web/packages/web-app-app-store/tests/unit/views/AppList.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { RouteLocationNormalizedLoaded } from 'vue-router'
import { setActivePinia } from 'pinia'
import {
createTestingPinia,
defaultComponentMocks,
defaultPlugins,
flushPromises,
shallowMount,
VueWrapper
} from '@ownclouders/web-test-helpers'
import AppList from '../../../src/views/AppList.vue'
import { useAppsStore } from '../../../src/piniaStores'
import { App } from '../../../src/types'

const selectors = {
noContentMessage: 'no-content-message-stub',
appTile: 'app-tile-stub',
filterInput: '#apps-filter'
}

const makeApp = (id: string, name: string): App => ({
id,
name,
subtitle: `${name} subtitle`,
license: 'MIT',
versions: [{ version: '1.0.0', url: 'https://example.test/app.zip' }],
authors: [{ name: 'ownCloud' }],
tags: ['productivity'],
screenshots: [],
resources: [],
repository: { name: 'repo', url: 'https://example.test/repo.json' },
mostRecentVersion: { version: '1.0.0', url: 'https://example.test/app.zip' }
})

describe('AppList', () => {
it('shows the no-content message when no apps match', () => {
const { wrapper } = getWrapper({ apps: [] })
expect(wrapper.find(selectors.noContentMessage).exists()).toBe(true)
expect(wrapper.findAll(selectors.appTile)).toHaveLength(0)
})

it('renders a tile for each app', () => {
const { wrapper } = getWrapper({ apps: [makeApp('alpha', 'Alpha'), makeApp('bravo', 'Bravo')] })
expect(wrapper.findAll(selectors.appTile)).toHaveLength(2)
expect(wrapper.find(selectors.noContentMessage).exists()).toBe(false)
})

it('reflects the search term into the filter route query on input', async () => {
const { wrapper, mocks } = getWrapper({ apps: [makeApp('alpha', 'Alpha')] })
await (wrapper.findComponent(selectors.filterInput) as VueWrapper).vm.$emit(
'update:modelValue',
'foo'
)
expect(mocks.$router.replace).toHaveBeenCalledWith({ query: { filter: 'foo' } })
})

it('narrows the rendered tiles to the ones matching the active filter query', async () => {
const { wrapper } = getWrapper({
apps: [makeApp('alpha', 'Alpha'), makeApp('bravo', 'Bravo')],
filter: 'Alpha'
})
await flushPromises()
const filteredApps = (wrapper.vm as unknown as { filteredApps: App[] }).filteredApps
expect(filteredApps.map((app) => app.name)).toEqual(['Alpha'])
expect(wrapper.findAll(selectors.appTile)).toHaveLength(1)
})
})

function getWrapper({ apps, filter = '' }: { apps: App[]; filter?: string }) {
const pinia = createTestingPinia({ stubActions: false })
setActivePinia(pinia)
const appsStore = useAppsStore()
appsStore.apps = apps
const plugins = [...defaultPlugins({ pinia: false }), pinia]

const mocks = {
...defaultComponentMocks({
currentRoute: {
name: 'app-store',
path: '/',
params: {},
query: { ...(filter && { filter }) },
meta: {}
} as unknown as RouteLocationNormalizedLoaded
})
}

return {
mocks,
wrapper: shallowMount(AppList, {
global: {
plugins,
mocks,
provide: mocks,
// render oc-list's default slot so the app tiles inside become visible stubs
stubs: { 'oc-list': { template: '<ul><slot /></ul>' } }
}
})
}
}
Loading