Skip to content

Commit

Permalink
Merge pull request #11752 from owncloud/app-store-unit-tests
Browse files Browse the repository at this point in the history
App store unit tests (first batch)
  • Loading branch information
kulmann authored Oct 11, 2024
2 parents b566cf7 + 0fdcd37 commit 3e6f30c
Show file tree
Hide file tree
Showing 14 changed files with 649 additions and 19 deletions.
2 changes: 1 addition & 1 deletion packages/web-app-app-store/src/LayoutContainer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<main id="app-store">
<app-loading-spinner v-if="areAppsLoading" />
<template v-else>
<router-view />
<router-view data-testid="app-store-router-view" />
</template>
</main>
</template>
Expand Down
31 changes: 27 additions & 4 deletions packages/web-app-app-store/src/components/AppAuthors.vue
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
<template>
<ul class="oc-mb-rm oc-p-rm">
<li v-for="author in app.authors" :key="author.name" class="app-author-item">
<a v-if="author.url" :href="author.url" target="_blank">{{ author.name }}</a>
<span v-else>{{ author.name }}</span>
<li v-for="author in authors" :key="author.name" class="app-author-item">
<a v-if="author.url" :href="author.url" data-testid="author-link" target="_blank">
{{ author.name }}
</a>
<span v-else data-testid="author-label">{{ author.name }}</span>
</li>
</ul>
</template>

<script lang="ts">
import { defineComponent, PropType } from 'vue'
import { computed, defineComponent, PropType } from 'vue'
import { App } from '../types'
import { isEmpty } from 'lodash-es'
export default defineComponent({
props: {
Expand All @@ -18,6 +21,26 @@ export default defineComponent({
required: true,
default: (): App => undefined
}
},
setup(props) {
const authors = computed(() => {
return (props.app.authors || []).filter((author) => {
if (isEmpty(author.name)) {
return false
}
if (!isEmpty(author.url)) {
try {
new URL(author.url)
} catch {
return false
}
}
return true
})
})
return {
authors
}
}
})
</script>
Expand Down
17 changes: 15 additions & 2 deletions packages/web-app-app-store/src/components/AppImageGallery.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,19 @@
</div>
<ul v-if="hasPagination" class="app-image-navigation">
<li>
<oc-button class="oc-p-xs" appearance="raw" variation="primary" @click="previousImage">
<oc-button
data-testid="prev-image"
class="oc-p-xs"
appearance="raw"
variation="primary"
@click="previousImage"
>
<oc-icon name="arrow-left-s" />
</oc-button>
</li>
<li v-for="(image, index) in images" :key="`gallery-page-${index}`">
<oc-button
data-testid="set-image"
class="oc-py-xs"
appearance="raw"
variation="primary"
Expand All @@ -30,7 +37,13 @@
</oc-button>
</li>
<li>
<oc-button class="oc-p-xs" appearance="raw" variation="primary" @click="nextImage">
<oc-button
data-testid="next-image"
class="oc-p-xs"
appearance="raw"
variation="primary"
@click="nextImage"
>
<oc-icon name="arrow-right-s" />
</oc-button>
</li>
Expand Down
41 changes: 36 additions & 5 deletions packages/web-app-app-store/src/components/AppResources.vue
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
<template>
<ul class="oc-mb-rm oc-p-rm">
<li v-for="resource in app.resources" :key="resource.label" class="app-resource-item">
<a :href="resource.url" target="_blank" class="oc-flex-inline oc-flex-middle">
<oc-icon v-if="resource.icon" :name="resource.icon" size="medium" class="oc-mr-xs" />
{{ resource.label }}
<li v-for="resource in resources" :key="resource.label" class="app-resource-item">
<a
:href="resource.url"
data-testid="resource-link"
target="_blank"
class="oc-flex-inline oc-flex-middle"
>
<oc-icon
v-if="resource.icon"
data-testid="resource-icon"
:name="resource.icon"
size="medium"
class="oc-mr-xs"
/>
<span data-testid="resource-label">{{ resource.label }}</span>
</a>
</li>
</ul>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
import { computed, defineComponent, PropType } from 'vue'
import { App } from '../types'
import { isEmpty } from 'lodash-es'
export default defineComponent({
name: 'AppResources',
Expand All @@ -20,6 +32,25 @@ export default defineComponent({
required: true,
default: (): App => undefined
}
},
setup(props) {
const resources = computed(() => {
return (props.app.resources || []).filter((resource) => {
if (isEmpty(resource.url) || isEmpty(resource.label)) {
return false
}
try {
new URL(resource.url)
} catch {
return false
}
return true
})
})
return {
resources
}
}
})
</script>
Expand Down
1 change: 1 addition & 0 deletions packages/web-app-app-store/src/components/AppTags.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<oc-tag
v-for="tag in app.tags"
:key="`app-tag-${app.id}-${tag}`"
data-testid="tag-button"
size="small"
class="oc-text-nowrap"
type="button"
Expand Down
27 changes: 20 additions & 7 deletions packages/web-app-app-store/src/components/AppVersions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { computed, defineComponent, PropType } from 'vue'
import { App } from '../types'
import { useGettext } from 'vue3-gettext'
import AppActions from './AppActions.vue'
import { isEmpty } from 'lodash-es'
export default defineComponent({
name: 'AppVersions',
Expand All @@ -32,13 +33,25 @@ export default defineComponent({
const { $gettext } = useGettext()
const data = computed(() => {
return props.app.versions.map((version) => {
return {
...version,
minOCIS: version.minOCIS ? `v${version.minOCIS}` : '-',
id: version.version
}
})
return (props.app.versions || [])
.filter((version) => {
if (isEmpty(version.version) || isEmpty(version.url)) {
return false
}
try {
new URL(version.url)
} catch {
return false
}
return true
})
.map((version) => {
return {
...version,
minOCIS: version.minOCIS ? `v${version.minOCIS}` : '-',
id: version.version
}
})
})
const fields = computed(() => {
return [
Expand Down
3 changes: 3 additions & 0 deletions packages/web-app-app-store/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@ export const AppBadgeSchema = z.object({
label: z.string(),
color: z.enum(BADGE_COLORS).optional().default('primary')
})
export type AppBadge = z.infer<typeof AppBadgeSchema>

export const AppAuthorSchema = z.object({
name: z.string(),
email: z.string().optional(),
url: z.string().optional()
})
export type AppAuthor = z.infer<typeof AppAuthorSchema>

export const AppImageSchema = z.object({
url: z.string(),
Expand All @@ -41,6 +43,7 @@ export const AppResourceSchema = z.object({
label: z.string(),
icon: z.string().optional()
})
export type AppResource = z.infer<typeof AppResourceSchema>

export const RawAppSchema = z.object({
id: z.string(),
Expand Down
57 changes: 57 additions & 0 deletions packages/web-app-app-store/tests/unit/LayoutContainer.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import LayoutContainer from '../../src/LayoutContainer.vue'
import {
defaultComponentMocks,
defaultPlugins,
mount,
nextTicks
} from '@ownclouders/web-test-helpers'
import { useAppsStore } from '../../src/piniaStores'

const selectors = {
loadingSpinner: '#app-loading-spinner',
routerView: '[data-testid="app-store-router-view"]'
}

describe('LayoutContainer', () => {
it('shows a loading spinner while apps are loading from repositories', () => {
const { wrapper } = getWrapper({ keepLoadingApps: true })
expect(wrapper.find(selectors.loadingSpinner).exists()).toBeTruthy()
expect(wrapper.find(selectors.routerView).exists()).toBeFalsy()
})
it('renders the router view when loading apps is done', async () => {
const { wrapper } = getWrapper({})
await nextTicks(2)
expect(wrapper.find(selectors.loadingSpinner).exists()).toBeFalsy()
expect(wrapper.find(selectors.routerView).exists()).toBeTruthy()
})
})

function getWrapper({ keepLoadingApps }: { keepLoadingApps?: boolean }) {
const plugins = defaultPlugins({})

const { loadApps } = useAppsStore()
vi.mocked(loadApps).mockReturnValue(
new Promise((res) => {
if (!keepLoadingApps) {
return res()
}
return setTimeout(() => res(), 500)
})
)

const mocks = {
...defaultComponentMocks(),
loadApps
}

return {
mocks,
wrapper: mount(LayoutContainer, {
global: {
mocks,
provide: mocks,
plugins
}
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { defaultPlugins, mount } from '@ownclouders/web-test-helpers'
import AppActions from '../../../src/components/AppActions.vue'
import { App, AppVersion } from '../../../src/types'
import { mock } from 'vitest-mock-extended'

const version1: AppVersion = {
version: '1.0.0',
url: 'https://example.com/app-1.0.0.zip'
}
const version2: AppVersion = {
version: '1.1.0',
url: 'https://example.com/app-1.1.0.zip'
}
const versions = [version1, version2]
const mostRecentVersion = version2

const selectors = {
downloadButton: 'button'
}

describe('AppActions', () => {
it('renders a "Download" button', () => {
const { wrapper } = getWrapper({})
expect(wrapper.find(selectors.downloadButton).text()).toBe('Download')
})
describe('calling the "download" handler', () => {
it('uses the most recent version when none is specified', async () => {
const { wrapper } = getWrapper({})
await wrapper.find(selectors.downloadButton).trigger('click')
expect(window.location.href).toBe(mostRecentVersion.url)
})
it('uses the version provided via props', async () => {
const { wrapper } = getWrapper({ version: version1 })
await wrapper.find(selectors.downloadButton).trigger('click')
expect(window.location.href).toBe(version1.url)
})
})
})

const getWrapper = ({ version }: { version?: AppVersion }) => {
const app = { ...mock<App>({}), versions, mostRecentVersion }

return {
wrapper: mount(AppActions, {
props: {
app,
version
},
global: {
plugins: [...defaultPlugins()]
}
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import AppAuthors from '../../../src/components/AppAuthors.vue'
import { mock } from 'vitest-mock-extended'
import { App, AppAuthor } from '../../../src/types'
import { mount } from '@ownclouders/web-test-helpers'

const author1: AppAuthor = {
name: 'John Doe',
url: 'https://johndoe.com'
}
const author2: AppAuthor = {
name: 'Jane Doe'
}
const author3: AppAuthor = {
name: 'Wololo Priest',
url: 'wololo'
}
const author4: AppAuthor = {
url: 'trololo'
}
const authors = [author1, author2, author3, author4]

const selectors = {
item: '.app-author-item',
link: '[data-testid="author-link"]',
label: '[data-testid="author-label"]'
}

describe('AppAuthors.vue', () => {
it('renders only authors with name and valid or empty url', () => {
const { wrapper } = getWrapper()
expect(wrapper.findAll(selectors.item).length).toBe(2)
})
it('renders authors as link when they have a url and name', () => {
const { wrapper } = getWrapper()
const author = wrapper.findAll(selectors.item).at(0)
expect(author.exists()).toBeTruthy()
const link = author.find(selectors.link)
expect(link.exists()).toBeTruthy()
expect(link.attributes().href).toBe(author1.url)
expect(link.text()).toBe(author1.name)
})
it('renders authors as span when they only have a name', () => {
const { wrapper } = getWrapper()
const author = wrapper.findAll(selectors.item).at(1)
expect(author.find(selectors.link).exists()).toBeFalsy()
expect(author.find(selectors.label).text()).toBe(author2.name)
})
})

const getWrapper = () => {
const app = { ...mock<App>({}), authors }

return {
wrapper: mount(AppAuthors, {
props: { app }
})
}
}
Loading

0 comments on commit 3e6f30c

Please sign in to comment.